Problem & Motivation
Today a tool is built with tool.subcommand(...) passing output and handler as separate fields (e.g. output: l3Output({...}) plus a separate handler). The handler is already typed HandlerReturn<O> and Tool.validateOutput() rejects a non-conforming return at runtime, so 'returning an object that does not conform' is already covered. The real gap is twofold: (1) output and handler are separate fields, so an l3Output(...) can be paired with a handler written for another layer, and the type only ties them once both are inside the same spec; (2) you must pick and type output by hand every time, which opens the door to choosing the wrong layer and adds repeated boilerplate. Goal: make building (and maintaining) a subcommand fix the layer from the way it is constructed, with no way to pick the wrong layer or repeat the output.
Scope & Decomposition
Add a per-layer helper as a chainable instance method, mirroring tool.subcommand: tool.l1Subcommand / tool.l2Subcommand / tool.l3Subcommand. Each fixes the layer output internally and forces, by types, that the handler returns the layer fields (does not compile if missing). The signature is NOT uniform across layers because each lXOutput needs different runtime inputs (the Zod schema must exist to validate; it cannot be inferred from the handler type): L1 takes a required fields; L2 takes a required classification enum plus optional fields; L3 takes optional fields (instructions is fixed). Each call is equivalent to tool.subcommand({ ..., output: lXOutput(...), handler }). Additive: tool.subcommand and the lXOutput helpers keep working as today. Binding and naming are decided: instance method, camelCase, chainable.
Acceptance Criteria
- The system exposes one method per layer (
tool.l1Subcommand / tool.l2Subcommand / tool.l3Subcommand). Each receives name/description/input/handler plus the layer-specific inputs (L1: fields; L2: classification + optional fields; L3: optional fields) and registers a subcommand equivalent to tool.subcommand with output fixed to the corresponding lXOutput(...).
- The helper handler signature forces, by types, returning the layer fields (e.g.
instructions in L3); it does not compile if missing. Verified with a type-level test (a @ts-expect-error fixture: an L3 handler without instructions must break compilation).
- The
output schema registered by tool.lXSubcommand(args) is equivalent to the one produced by lXOutput(...) with the same inputs (runtime-verifiable by comparing registered schemas).
tool.subcommand and the lXOutput helpers keep working as today (non-breaking; the helper is additive).
- Editing the handler of a subcommand created via the helper keeps enforcing the layer type (same type-level test as above).
Risks & Constraints
Low risk: additive, does not break the current API. Main risk: duplicated API surface (two ways to build a subcommand). Mitigation: document the helper as the recommended path and raw tool.subcommand as the low-level one.
[inferred] The L2/L3 lXOutput overloads must keep producing the same base shape whether or not optional fields are passed, otherwise the schema-equivalence check is ambiguous.
Edge Cases
l1Output requires fields (no no-arg overload) and l2Output requires the classification enum, so a uniform {name, description, input, handler} signature cannot build L1 or L2 outputs; the per-layer signature must carry those inputs.
[inferred] Argless subcommands at L1 still need an explicit fields shape (even {}); confirm l1Subcommand accepts an empty fields object.
Out of Scope
- Error model /
ok: false (already exists in src/envelopes.ts).
- Adapter to external SDKs (eve/claude): separate issue.
- Package rename: parked.
Problem & Motivation
Today a tool is built with
tool.subcommand(...)passingoutputandhandleras separate fields (e.g.output: l3Output({...})plus a separatehandler). The handler is already typedHandlerReturn<O>andTool.validateOutput()rejects a non-conforming return at runtime, so 'returning an object that does not conform' is already covered. The real gap is twofold: (1)outputandhandlerare separate fields, so anl3Output(...)can be paired with a handler written for another layer, and the type only ties them once both are inside the same spec; (2) you must pick and typeoutputby hand every time, which opens the door to choosing the wrong layer and adds repeated boilerplate. Goal: make building (and maintaining) a subcommand fix the layer from the way it is constructed, with no way to pick the wrong layer or repeat the output.Scope & Decomposition
Add a per-layer helper as a chainable instance method, mirroring
tool.subcommand:tool.l1Subcommand/tool.l2Subcommand/tool.l3Subcommand. Each fixes the layeroutputinternally and forces, by types, that the handler returns the layer fields (does not compile if missing). The signature is NOT uniform across layers because eachlXOutputneeds different runtime inputs (the Zod schema must exist to validate; it cannot be inferred from the handler type): L1 takes a requiredfields; L2 takes a requiredclassificationenum plus optionalfields; L3 takes optionalfields(instructions is fixed). Each call is equivalent totool.subcommand({ ..., output: lXOutput(...), handler }). Additive:tool.subcommandand thelXOutputhelpers keep working as today. Binding and naming are decided: instance method, camelCase, chainable.Acceptance Criteria
tool.l1Subcommand/tool.l2Subcommand/tool.l3Subcommand). Each receivesname/description/input/handlerplus the layer-specific inputs (L1:fields; L2:classification+ optionalfields; L3: optionalfields) and registers a subcommand equivalent totool.subcommandwithoutputfixed to the correspondinglXOutput(...).instructionsin L3); it does not compile if missing. Verified with a type-level test (a@ts-expect-errorfixture: an L3 handler withoutinstructionsmust break compilation).outputschema registered bytool.lXSubcommand(args)is equivalent to the one produced bylXOutput(...)with the same inputs (runtime-verifiable by comparing registered schemas).tool.subcommandand thelXOutputhelpers keep working as today (non-breaking; the helper is additive).Risks & Constraints
Low risk: additive, does not break the current API. Main risk: duplicated API surface (two ways to build a subcommand). Mitigation: document the helper as the recommended path and raw
tool.subcommandas the low-level one.[inferred] The L2/L3
lXOutputoverloads must keep producing the same base shape whether or not optionalfieldsare passed, otherwise the schema-equivalence check is ambiguous.Edge Cases
l1Outputrequiresfields(no no-arg overload) andl2Outputrequires theclassificationenum, so a uniform{name, description, input, handler}signature cannot build L1 or L2 outputs; the per-layer signature must carry those inputs.[inferred] Argless subcommands at L1 still need an explicit
fieldsshape (even{}); confirml1Subcommandaccepts an emptyfieldsobject.Out of Scope
ok: false(already exists insrc/envelopes.ts).