This document describes how runtime schemas are organized in sim-one-alpha, why we chose Valibot, and when to promote a schema to a shared location.
Types alone cannot validate data at runtime. Flue supports structured output via session.task(..., { result: Schema }), tools receive untrusted arguments from the model, and records in the repository’s SQLite and LanceDB stores must be validated at their read and write boundaries. The same rule applies when another persistence backend is added. A single source of truth for each shape prevents the TypeScript type and the runtime validator from drifting apart.
We use Valibot because:
@flue/runtime uses Valibot for session.prompt, session.skill, and session.task structured-result overloads. Staying aligned with Flue avoids adding a second validation library.v.InferOutput<typeof Schema> gives us a TS type from the same source as the runtime validator.@valibot/to-json-schema package (already a transitive dependency) can convert Valibot schemas to JSON Schema or OpenAPI when we need API documentation or generated forms.src/core/schemas/
Shared Valibot schemas for cross-cutting runtime contracts.
One file per domain. Promote a schema here only when it is reused across files or subsystems.
src/engine/workers/<domain>/schemas.ts (optional)
Worker-local schemas that are only consumed inside that worker.
Useful when a schema is private to a single tool or workflow.
src/core/types/
Pure TypeScript type contracts. May re-export inferred types from
`src/core/schemas/` so consumers can continue importing from one place.
Example: a one-off validation schema inside a single tool file.
src/core/schemas/ when:session.task(..., { result: ... })) and the type is referenced in a shared type contract.Example: CodingImplementerResultSchema lives in src/core/schemas/coding-worker.ts because it is shared between the implementer tool, the delegation path, and the public CodingSubagentRunResult type contract.
Define the schema and derive its type in the schema file:
// src/core/schemas/coding-worker.ts
import * as v from 'valibot';
export const CodingImplementerResultSchema = v.object({
fileEdits: v.array(
v.object({
path: v.string(),
oldText: v.string(),
newText: v.string(),
expectedOccurrences: v.optional(v.number()),
})
),
writeFiles: v.array(
v.object({
path: v.string(),
content: v.string(),
})
),
verificationCommands: v.array(
v.object({
name: v.string(),
command: v.string(),
required: v.optional(v.boolean()),
reason: v.optional(v.string()),
cwd: v.optional(v.string()),
timeoutSeconds: v.optional(v.number()),
})
),
});
export type CodingImplementerResult = v.InferOutput<typeof CodingImplementerResultSchema>;
Import and re-export from the domain type contract so existing consumers do not need to change paths:
// src/engine/workers/coding-worker/types.ts
import { CodingImplementerResultSchema } from '../../../core/schemas/coding-worker.js';
export { CodingImplementerResultSchema };
export type CodingImplementerResult = import('../../../core/schemas/coding-worker.js').CodingImplementerResult;
Use the schema for structured Flue output:
// src/engine/workers/coding-worker/workflow/coordination.ts
import { CodingImplementerResultSchema } from '../../../../core/schemas/coding-worker.js';
const response = await session.task(prompt, {
agent: codingImplementerSubagentName,
result: CodingImplementerResultSchema,
});
return {
subagent: 'implementer',
summary: `Implementer submitted ${response.data.fileEdits.length} edit(s)...`,
evidence: [agent, childSession],
structuredOutput: { type: 'implementer', result: response.data },
};
src/core/schemas/ as contracts stabilize.src/core/schemas/<domain>/*.ts with an index.ts barrel.src/core/schemas/ file for a schema that is purely internal to one function; that belongs next to its consumer.src/core/schemas/.SIM-ONE governs memory, security, and development process. The schema layer supports SIM-ONE by: