@popcomputer/structured-chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +780 -0
  3. package/dist/adapters/openai-compatible-model.d.ts +89 -0
  4. package/dist/adapters/openai-compatible-model.js +231 -0
  5. package/dist/core/answer.d.ts +73 -0
  6. package/dist/core/answer.js +60 -0
  7. package/dist/core/chat.d.ts +124 -0
  8. package/dist/core/chat.js +490 -0
  9. package/dist/core/collect-stage.d.ts +203 -0
  10. package/dist/core/collect-stage.js +645 -0
  11. package/dist/core/command.d.ts +16 -0
  12. package/dist/core/command.js +17 -0
  13. package/dist/core/definition.d.ts +10 -0
  14. package/dist/core/definition.js +14 -0
  15. package/dist/core/json-value.d.ts +11 -0
  16. package/dist/core/json-value.js +3 -0
  17. package/dist/core/model-guard.d.ts +52 -0
  18. package/dist/core/model-guard.js +37 -0
  19. package/dist/core/model.d.ts +99 -0
  20. package/dist/core/model.js +109 -0
  21. package/dist/core/protocol.d.ts +257 -0
  22. package/dist/core/protocol.js +153 -0
  23. package/dist/core/question.d.ts +52 -0
  24. package/dist/core/question.js +62 -0
  25. package/dist/core/repair.d.ts +14 -0
  26. package/dist/core/repair.js +9 -0
  27. package/dist/core/session.d.ts +78 -0
  28. package/dist/core/session.js +34 -0
  29. package/dist/core/stage-name.d.ts +3 -0
  30. package/dist/core/stage-name.js +3 -0
  31. package/dist/core/stage.d.ts +88 -0
  32. package/dist/core/stage.js +104 -0
  33. package/dist/core/tool-set.d.ts +49 -0
  34. package/dist/core/tool-set.js +66 -0
  35. package/dist/core/tool.d.ts +149 -0
  36. package/dist/core/tool.js +215 -0
  37. package/dist/core/view.d.ts +95 -0
  38. package/dist/core/view.js +69 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.js +16 -0
  41. package/dist/integrations/assistant-ui.d.ts +95 -0
  42. package/dist/integrations/assistant-ui.js +114 -0
  43. package/dist/testing/in-memory-session-store.d.ts +4 -0
  44. package/dist/testing/in-memory-session-store.js +38 -0
  45. package/dist/testing/scenario.d.ts +59 -0
  46. package/dist/testing/scenario.js +147 -0
  47. package/dist/testing.d.ts +4 -0
  48. package/dist/testing.js +2 -0
  49. package/examples/agency-search.ts +101 -0
  50. package/examples/answer-modes.ts +92 -0
  51. package/examples/prompt-injection-policy.ts +66 -0
  52. package/package.json +89 -0
@@ -0,0 +1,49 @@
1
+ import { Effect } from "effect";
2
+ import type { InvalidToolCall, InvalidToolProjection, ModelToolDefinition, QueryToolDefinitionContract, StructuredTool, ToolDefinitionContract, ToolExecution, ToolCall } from "./tool.js";
3
+ import { type JsonValue } from "./json-value.js";
4
+ /** Non-empty tuple of model-callable query or command definitions. */
5
+ export type ModelToolTuple = readonly [
6
+ ToolDefinitionContract,
7
+ ...ReadonlyArray<ToolDefinitionContract>
8
+ ];
9
+ /** Non-empty tuple accepted by one repeatable closed query set. */
10
+ export type ToolTuple = readonly [
11
+ QueryToolDefinitionContract,
12
+ ...ReadonlyArray<QueryToolDefinitionContract>
13
+ ];
14
+ type ToolExecutionOf<Tool> = Tool extends StructuredTool<infer _Name, infer _InputSchema, infer ServerResult, infer _Error, infer _Requirements, infer ModelSchema, infer Presenters, infer _Operation> ? ToolExecution<ServerResult, ModelSchema, Presenters> : never;
15
+ type ToolCallOf<Tool> = Tool extends StructuredTool<infer Name, infer InputSchema, infer _ServerResult, infer _Error, infer _Requirements, infer _ModelSchema, infer _Presenters, infer _Operation> ? ToolCall<Name, InputSchema> : never;
16
+ /** Parsed call union accepted by any member of one tool set. */
17
+ export type ToolSetCall<Tools extends ModelToolTuple> = ToolCallOf<Tools[number]>;
18
+ type ToolErrorOf<Tool> = Tool extends StructuredTool<infer _Name, infer _InputSchema, infer _ServerResult, infer Error, infer _Requirements, infer _ModelSchema, infer _Presenters, infer _Operation> ? Error : never;
19
+ type ToolRequirementsOf<Tool> = Tool extends StructuredTool<infer _Name, infer _InputSchema, infer _ServerResult, infer _Error, infer Requirements, infer _ModelSchema, infer _Presenters, infer _Operation> ? Requirements : never;
20
+ /** Execution union produced by any member of a tool set. */
21
+ export type ToolSetExecution<Tools extends ToolTuple> = ToolExecutionOf<Tools[number]>;
22
+ /** Application failure union produced by any member of a tool set. */
23
+ export type ToolSetError<Tools extends ToolTuple> = InvalidToolCall | InvalidToolProjection | ToolErrorOf<Tools[number]>;
24
+ /** Effect services required by any member of a tool set. */
25
+ export type ToolSetRequirements<Tools extends ToolTuple> = ToolRequirementsOf<Tools[number]>;
26
+ /** Planning-only registry shared by query and command stages. */
27
+ export interface ToolCallPlanner<Tools extends ModelToolTuple> {
28
+ readonly models: ReadonlyArray<ModelToolDefinition>;
29
+ readonly parseCall: (input: JsonValue) => Effect.Effect<ToolSetCall<Tools>, InvalidToolCall>;
30
+ }
31
+ /** A closed, stage-safe registry of tools that may execute. */
32
+ export interface ToolSet<Tools extends ToolTuple> extends ToolCallPlanner<Tools> {
33
+ readonly tools: Tools;
34
+ readonly models: ReadonlyArray<ModelToolDefinition>;
35
+ /** Strictly parse one call to one registered tool without executing it. */
36
+ readonly parseCall: (input: JsonValue) => Effect.Effect<ToolSetCall<Tools>, InvalidToolCall>;
37
+ /** Execute one already-parsed call without decoding its arguments again. */
38
+ readonly execute: (call: ToolSetCall<Tools>) => Effect.Effect<ToolSetExecution<Tools>, ToolSetError<Tools>, ToolSetRequirements<Tools>>;
39
+ /** Strictly parse and execute one call to one registered tool. */
40
+ readonly executeCall: (input: JsonValue) => Effect.Effect<ToolSetExecution<Tools>, ToolSetError<Tools>, ToolSetRequirements<Tools>>;
41
+ }
42
+ /**
43
+ * Define the complete set of tools available to one model step or chat stage.
44
+ *
45
+ * Names must be unique. Unknown, malformed, and out-of-stage calls fail before
46
+ * application execution begins.
47
+ */
48
+ export declare const defineToolSet: <const Tools extends ToolTuple>(...tools: Tools) => ToolSet<Tools>;
49
+ export {};
@@ -0,0 +1,66 @@
1
+ import { Effect, Schema, unsafeCoerce } from "effect";
2
+ import { InvalidToolCall as InvalidToolCallError, InvalidToolCallReasonSchema, ToolNameSchema, } from "./tool.js";
3
+ import { JsonValueSchema } from "./json-value.js";
4
+ const IncomingToolCallSchema = Schema.Struct({
5
+ name: ToolNameSchema,
6
+ arguments: JsonValueSchema,
7
+ });
8
+ const invalidCall = (reason, tool) => new InvalidToolCallError({
9
+ tool,
10
+ reason,
11
+ path: null,
12
+ });
13
+ /**
14
+ * Define the complete set of tools available to one model step or chat stage.
15
+ *
16
+ * Names must be unique. Unknown, malformed, and out-of-stage calls fail before
17
+ * application execution begins.
18
+ */
19
+ export const defineToolSet = (...tools) => {
20
+ const runtimeTools = new Map();
21
+ for (const tool of tools) {
22
+ if (tool.operation !== "query") {
23
+ throw new Error("Repeatable tool sets accept query tools only");
24
+ }
25
+ if (runtimeTools.has(tool.name)) {
26
+ throw new Error(`Duplicate structured chat tool name: ${tool.name}`);
27
+ }
28
+ // SAFETY: ToolDefinitionContract carries the package-owned nominal identity.
29
+ // Runtime execution is erased here and restored by ToolSet's conditional
30
+ // public result, error, and requirement types.
31
+ runtimeTools.set(tool.name, unsafeCoerce(tool));
32
+ }
33
+ const selectTool = (name) => {
34
+ const tool = runtimeTools.get(name);
35
+ if (tool === undefined) {
36
+ return Effect.fail(invalidCall("unknown_tool", name));
37
+ }
38
+ return Effect.succeed(tool);
39
+ };
40
+ const parseCallRuntime = (input) => Schema.decodeUnknown(IncomingToolCallSchema)(input, {
41
+ onExcessProperty: "error",
42
+ }).pipe(Effect.mapError(() => invalidCall("invalid_envelope", null)), Effect.flatMap((call) => selectTool(call.name).pipe(Effect.flatMap((tool) => tool.parseCall(call)))), Effect.withSpan("popcomputer.structured_chat.tool_set.parse", {
43
+ attributes: { toolCount: tools.length },
44
+ }));
45
+ // SAFETY: the envelope parser establishes a registered name, then that
46
+ // registered tool parses its own literal name and argument schema.
47
+ const parseCall = parseCallRuntime;
48
+ const executeRuntime = (call) => selectTool(call.name).pipe(Effect.flatMap((tool) => tool.execute(call.arguments)), Effect.withSpan("popcomputer.structured_chat.tool_set.execute", {
49
+ attributes: { toolCount: tools.length },
50
+ }));
51
+ // SAFETY: call is a parsed member of ToolSetCall<Tools>; dispatch selects
52
+ // that member's registered runtime and passes its decoded arguments to the
53
+ // corresponding Type-side execute operation.
54
+ const execute = executeRuntime;
55
+ const executeCall = (input) => parseCall(input).pipe(Effect.flatMap(execute));
56
+ return {
57
+ tools,
58
+ models: tools.map(({ model }) => model),
59
+ parseCall,
60
+ execute,
61
+ // SAFETY: dispatch selects a member of Tools by its unique runtime name.
62
+ // Each tool parses itself before executing, so the resulting union exactly
63
+ // matches ToolSetExecution, ToolSetError, and ToolSetRequirements.
64
+ executeCall: executeCall,
65
+ };
66
+ };
@@ -0,0 +1,149 @@
1
+ import { Effect, JSONSchema, Pipeable, Schema } from "effect";
2
+ import * as ParseResult from "effect/ParseResult";
3
+ import type { ViewDefinitionContract, ViewInput, ViewPart } from "./view.js";
4
+ import { type StructuredDefinition } from "./definition.js";
5
+ import type { CommandId } from "./command.js";
6
+ import type { JsonValue } from "./json-value.js";
7
+ /** Stable machine-facing name for one structured chat tool. */
8
+ export declare const ToolNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
9
+ /** Bounded model-facing description for one structured chat tool. */
10
+ export declare const ToolDescriptionSchema: Schema.filter<typeof Schema.NonEmptyTrimmedString>;
11
+ /** Safe reason that a model-authored tool call was rejected. */
12
+ export declare const InvalidToolCallReasonSchema: Schema.Literal<["invalid_envelope", "unknown_tool", "invalid_arguments"]>;
13
+ declare const InvalidToolCall_base: Schema.TaggedErrorClass<InvalidToolCall, "InvalidToolCall", {
14
+ readonly _tag: Schema.tag<"InvalidToolCall">;
15
+ } & {
16
+ tool: Schema.NullOr<Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>>;
17
+ reason: Schema.Literal<["invalid_envelope", "unknown_tool", "invalid_arguments"]>;
18
+ path: Schema.NullOr<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
19
+ }>;
20
+ /** A model-authored tool call failed strict parsing. */
21
+ export declare class InvalidToolCall extends InvalidToolCall_base {
22
+ }
23
+ /** Safe reason that an application-owned tool projection was rejected. */
24
+ export declare const InvalidToolProjectionReasonSchema: Schema.Literal<["invalid_model_result", "invalid_view_data"]>;
25
+ declare const InvalidToolProjection_base: Schema.TaggedErrorClass<InvalidToolProjection, "InvalidToolProjection", {
26
+ readonly _tag: Schema.tag<"InvalidToolProjection">;
27
+ } & {
28
+ tool: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
29
+ target: typeof Schema.String;
30
+ reason: Schema.Literal<["invalid_model_result", "invalid_view_data"]>;
31
+ }>;
32
+ /** An application-owned model or view projection violated its schema. */
33
+ export declare class InvalidToolProjection extends InvalidToolProjection_base {
34
+ }
35
+ /** Provider-neutral model tool definition derived from Effect Schema. */
36
+ export interface ModelToolDefinition<Name extends string = string> {
37
+ readonly name: Name;
38
+ readonly description: string;
39
+ readonly inputSchema: JSONSchema.JsonSchema7Root;
40
+ }
41
+ /** Whether an executable model capability is repeatable or side-effecting. */
42
+ export type ToolOperation = "query" | "command";
43
+ /** Opaque stable identity supplied when a command executes. */
44
+ export interface CommandExecutionContext {
45
+ readonly commandId: CommandId;
46
+ }
47
+ /** Minimum runtime shape retained for every structured chat tool. */
48
+ export interface ToolDefinitionContract extends StructuredDefinition<"tool"> {
49
+ readonly _tag: "StructuredTool";
50
+ readonly operation: ToolOperation;
51
+ readonly name: string;
52
+ readonly description: string;
53
+ readonly inputSchema: Schema.Schema.AnyNoContext;
54
+ readonly model: ModelToolDefinition;
55
+ }
56
+ /** Minimum runtime shape retained for a repeatable read-only query. */
57
+ export interface QueryToolDefinitionContract extends ToolDefinitionContract {
58
+ readonly operation: "query";
59
+ }
60
+ /** Minimum runtime shape retained for a side-effecting command. */
61
+ export interface CommandDefinitionContract extends ToolDefinitionContract {
62
+ readonly operation: "command";
63
+ readonly parseCall: (input: JsonValue) => Effect.Effect<ToolCall<string, Schema.Schema.AnyNoContext>, InvalidToolCall>;
64
+ }
65
+ /** One model-authored, schema-parsed tool call. */
66
+ export type ToolCall<Name extends string, InputSchema extends Schema.Schema.AnyNoContext> = {
67
+ readonly name: Name;
68
+ readonly arguments: Schema.Schema.Type<InputSchema>;
69
+ };
70
+ /** One application-owned view projection attached to a tool. */
71
+ export interface ToolPresenter<ServerResult, View extends ViewDefinitionContract> {
72
+ readonly view: View;
73
+ readonly project: (result: ServerResult) => ViewInput<View> | undefined;
74
+ }
75
+ type PresenterView<Presenter> = Presenter extends ToolPresenter<infer _ServerResult, infer View> ? View : never;
76
+ /** View-part union produced by one configured tool. */
77
+ export type ToolViewPart<Presenters extends ReadonlyArray<ToolPresenter<never, ViewDefinitionContract>>> = ViewPart<PresenterView<Presenters[number]>>;
78
+ type ToolModelResult<ModelSchema extends Schema.Schema.AnyNoContext | undefined> = ModelSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ModelSchema> : undefined;
79
+ /** Complete trusted result of one parsed and executed tool call. */
80
+ export interface ToolExecution<ServerResult, ModelSchema extends Schema.Schema.AnyNoContext | undefined, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>>> {
81
+ readonly serverResult: ServerResult;
82
+ readonly modelResult: ToolModelResult<ModelSchema>;
83
+ readonly views: ReadonlyArray<ToolViewPart<Presenters>>;
84
+ readonly [toolExecutionModelContext]: string | undefined;
85
+ }
86
+ interface ToolModelProjection<ServerResult, ModelSchema extends Schema.Schema.AnyNoContext> {
87
+ readonly schema: ModelSchema;
88
+ readonly project: (result: ServerResult) => Schema.Schema.Type<ModelSchema>;
89
+ }
90
+ interface ToolRuntime<Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements, ModelSchema extends Schema.Schema.AnyNoContext | undefined, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>>, Operation extends ToolOperation> {
91
+ readonly executeServer: (input: Schema.Schema.Type<InputSchema>, context: Operation extends "command" ? CommandExecutionContext : undefined) => Effect.Effect<ServerResult, Error, Requirements>;
92
+ readonly modelProjection: ModelSchema extends Schema.Schema.AnyNoContext ? ToolModelProjection<ServerResult, ModelSchema> : undefined;
93
+ readonly presenters: Presenters;
94
+ readonly callSchema: Schema.Schema.AnyNoContext;
95
+ readonly name: Name;
96
+ readonly description: string;
97
+ readonly inputSchema: InputSchema;
98
+ readonly operation: Operation;
99
+ }
100
+ declare const toolRuntime: unique symbol;
101
+ declare const toolExecutionModelContext: unique symbol;
102
+ interface RuntimeToolExecutionContext {
103
+ readonly [toolExecutionModelContext]?: string | undefined;
104
+ }
105
+ /** @internal Read the bounded model-visible context retained by one execution. */
106
+ export declare const readToolExecutionModelContext: (execution: RuntimeToolExecutionContext) => string | undefined;
107
+ /** One schema-defined, executable, and pipeable query tool. */
108
+ export interface StructuredTool<Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements, ModelSchema extends Schema.Schema.AnyNoContext | undefined = undefined, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>> = readonly [], Operation extends ToolOperation = "query"> extends Pipeable.Pipeable, ToolDefinitionContract {
109
+ readonly name: Name;
110
+ readonly operation: Operation;
111
+ readonly inputSchema: InputSchema;
112
+ readonly callSchema: Schema.Schema<ToolCall<Name, InputSchema>>;
113
+ readonly model: ModelToolDefinition<Name>;
114
+ /** Parse an unknown model-authored invocation. */
115
+ readonly parseCall: (input: JsonValue) => Effect.Effect<ToolCall<Name, InputSchema>, InvalidToolCall>;
116
+ /** Execute already-parsed arguments and produce trusted projections. */
117
+ readonly execute: Operation extends "command" ? (input: Schema.Schema.Type<InputSchema>, context: CommandExecutionContext) => Effect.Effect<ToolExecution<ServerResult, ModelSchema, Presenters>, Error | InvalidToolProjection, Requirements> : (input: Schema.Schema.Type<InputSchema>) => Effect.Effect<ToolExecution<ServerResult, ModelSchema, Presenters>, Error | InvalidToolProjection, Requirements>;
118
+ /** Parse and execute one unknown model-authored invocation. */
119
+ readonly executeCall: Operation extends "command" ? (input: JsonValue, context: CommandExecutionContext) => Effect.Effect<ToolExecution<ServerResult, ModelSchema, Presenters>, Error | InvalidToolCall | InvalidToolProjection, Requirements> : (input: JsonValue) => Effect.Effect<ToolExecution<ServerResult, ModelSchema, Presenters>, Error | InvalidToolCall | InvalidToolProjection, Requirements>;
120
+ readonly [toolRuntime]: ToolRuntime<Name, InputSchema, ServerResult, Error, Requirements, ModelSchema, Presenters, Operation>;
121
+ }
122
+ /** One side-effecting capability accepted only by a terminal command stage. */
123
+ export type StructuredCommand<Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements, ModelSchema extends Schema.Schema.AnyNoContext | undefined = undefined, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>> = readonly []> = StructuredTool<Name, InputSchema, ServerResult, Error, Requirements, ModelSchema, Presenters, "command">;
124
+ /** Definition input for one read-only structured chat tool. */
125
+ export interface DefineToolInput<Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements> {
126
+ readonly name: Name;
127
+ readonly description: string;
128
+ readonly input: InputSchema;
129
+ readonly execute: (input: Schema.Schema.Type<InputSchema>) => Effect.Effect<ServerResult, Error, Requirements>;
130
+ }
131
+ /** Definition input for one idempotently executed structured command. */
132
+ export interface DefineCommandInput<Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements> {
133
+ readonly name: Name;
134
+ readonly description: string;
135
+ readonly input: InputSchema;
136
+ readonly execute: (input: Schema.Schema.Type<InputSchema>, context: CommandExecutionContext) => Effect.Effect<ServerResult, Error, Requirements>;
137
+ }
138
+ /** Define one read-only, schema-validated structured chat tool. */
139
+ export declare const defineTool: <const Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements>(definition: DefineToolInput<Name, InputSchema, ServerResult, Error, Requirements>) => StructuredTool<Name, InputSchema, ServerResult, Error, Requirements>;
140
+ /** Define one side-effecting command requiring a stable idempotency key. */
141
+ export declare const defineCommand: <const Name extends string, InputSchema extends Schema.Schema.AnyNoContext, ServerResult, Error, Requirements>(definition: DefineCommandInput<Name, InputSchema, ServerResult, Error, Requirements>) => StructuredCommand<Name, InputSchema, ServerResult, Error, Requirements>;
142
+ /** Combinators that add optional capabilities to a structured tool. */
143
+ export declare const Tool: {
144
+ readonly modelResult: <ServerResult, ModelSchema extends Schema.Schema.AnyNoContext>(schema: ModelSchema, project: (result: ServerResult) => Schema.Schema.Type<ModelSchema>) => <Name extends string, InputSchema extends Schema.Schema.AnyNoContext, Error, Requirements, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>>, Operation extends ToolOperation>(tool: StructuredTool<Name, InputSchema, ServerResult, Error, Requirements, undefined, Presenters, Operation>) => StructuredTool<Name, InputSchema, ServerResult, Error, Requirements, ModelSchema, Presenters, Operation>;
145
+ readonly present: <ServerResult, View extends ViewDefinitionContract>(view: View, project: (result: ServerResult) => ViewInput<View> | undefined) => <Name extends string, InputSchema extends Schema.Schema.AnyNoContext, Error, Requirements, ModelSchema extends Schema.Schema.AnyNoContext | undefined, Presenters extends ReadonlyArray<ToolPresenter<ServerResult, ViewDefinitionContract>>, Operation extends ToolOperation>(tool: StructuredTool<Name, InputSchema, ServerResult, Error, Requirements, ModelSchema, Presenters, Operation>) => StructuredTool<Name, InputSchema, ServerResult, Error, Requirements, ModelSchema, readonly [...Presenters, ToolPresenter<ServerResult, View>], Operation>;
146
+ };
147
+ /** Parse failure retained only for documentation of owned schema boundaries. */
148
+ export type ToolBoundaryParseError = ParseResult.ParseError;
149
+ export {};
@@ -0,0 +1,215 @@
1
+ import { Effect, JSONSchema, Pipeable, Schema, unsafeCoerce, } from "effect";
2
+ import * as ParseResult from "effect/ParseResult";
3
+ import { structuredDefinition, } from "./definition.js";
4
+ /** Stable machine-facing name for one structured chat tool. */
5
+ export const ToolNameSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100), Schema.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
6
+ /** Bounded model-facing description for one structured chat tool. */
7
+ export const ToolDescriptionSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
8
+ /** Safe reason that a model-authored tool call was rejected. */
9
+ export const InvalidToolCallReasonSchema = Schema.Literal("invalid_envelope", "unknown_tool", "invalid_arguments");
10
+ /** A model-authored tool call failed strict parsing. */
11
+ export class InvalidToolCall extends Schema.TaggedError()("InvalidToolCall", {
12
+ tool: Schema.NullOr(ToolNameSchema),
13
+ reason: InvalidToolCallReasonSchema,
14
+ path: Schema.NullOr(Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500))),
15
+ }) {
16
+ }
17
+ /** Safe reason that an application-owned tool projection was rejected. */
18
+ export const InvalidToolProjectionReasonSchema = Schema.Literal("invalid_model_result", "invalid_view_data");
19
+ /** An application-owned model or view projection violated its schema. */
20
+ export class InvalidToolProjection extends Schema.TaggedError()("InvalidToolProjection", {
21
+ tool: ToolNameSchema,
22
+ target: Schema.String,
23
+ reason: InvalidToolProjectionReasonSchema,
24
+ }) {
25
+ }
26
+ const toolRuntime = Symbol("@popcomputer/structured-chat/ToolRuntime");
27
+ const toolExecutionModelContext = Symbol("@popcomputer/structured-chat/ToolExecutionModelContext");
28
+ const ToolExecutionModelContextSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(40_000));
29
+ /** @internal Read the bounded model-visible context retained by one execution. */
30
+ export const readToolExecutionModelContext = (execution) => {
31
+ // SAFETY: Every execution returned by a StructuredTool is constructed by
32
+ // makeTool below with this package-private symbol property.
33
+ const runtime = execution;
34
+ return runtime[toolExecutionModelContext];
35
+ };
36
+ const parseToolCall = (name, callSchema, input) => Schema.decodeUnknown(callSchema)(input, {
37
+ onExcessProperty: "error",
38
+ }).pipe(Effect.mapError((error) => {
39
+ const issue = ParseResult.ArrayFormatter.formatErrorSync(error)[0];
40
+ const issuePath = issue === undefined || issue.path.length === 0
41
+ ? "#"
42
+ : `#/${issue.path.map(String).join("/")}`;
43
+ return new InvalidToolCall({
44
+ tool: name,
45
+ reason: "invalid_arguments",
46
+ path: issuePath,
47
+ });
48
+ }));
49
+ const projectModelResult = (tool, projection, result) => {
50
+ if (projection === undefined) {
51
+ // SAFETY: The conditional result type is exactly undefined when no model
52
+ // projection schema is configured.
53
+ return Effect.succeed(undefined);
54
+ }
55
+ const invalidProjection = () => new InvalidToolProjection({
56
+ tool,
57
+ target: "model",
58
+ reason: "invalid_model_result",
59
+ });
60
+ return Effect.try({
61
+ try: () => projection.project(result),
62
+ catch: invalidProjection,
63
+ }).pipe(Effect.flatMap((projected) => Schema.validate(projection.schema)(projected, {
64
+ onExcessProperty: "error",
65
+ })), Effect.mapError(invalidProjection),
66
+ // SAFETY: Schema.validate returned the configured model schema's exact
67
+ // Type side, which is ToolModelResult<ModelSchema> in this branch.
68
+ Effect.map((value) => value));
69
+ };
70
+ const encodeToolExecutionModelContext = (tool, projection, modelResult) => {
71
+ if (projection === undefined) {
72
+ return Effect.succeed(undefined);
73
+ }
74
+ return Schema.encodeUnknown(projection.schema)(modelResult).pipe(Effect.flatMap((encodedResult) => Schema.encodeUnknown(Schema.parseJson())({
75
+ tool,
76
+ result: encodedResult,
77
+ })), Effect.flatMap((context) => Schema.decodeUnknown(ToolExecutionModelContextSchema)(context)), Effect.mapError(() => new InvalidToolProjection({
78
+ tool,
79
+ target: "model_context",
80
+ reason: "invalid_model_result",
81
+ })));
82
+ };
83
+ const projectViews = (tool, presenters, result) => Effect.forEach(presenters, (presenter) => {
84
+ const invalidProjection = () => new InvalidToolProjection({
85
+ tool,
86
+ target: presenter.view.name,
87
+ reason: "invalid_view_data",
88
+ });
89
+ return Effect.try({
90
+ try: () => presenter.project(result),
91
+ catch: invalidProjection,
92
+ }).pipe(Effect.flatMap((projected) => projected === undefined
93
+ ? Effect.succeed(undefined)
94
+ : presenter.view.parseData(projected)), Effect.mapError(invalidProjection));
95
+ }).pipe(Effect.map((parts) => parts.flatMap((part) => part === undefined ? [] : [part])), Effect.map((parts) => {
96
+ // SAFETY: Every retained part was parsed by the corresponding presenter
97
+ // view, and the output union is derived from that same presenter tuple.
98
+ return unsafeCoerce(parts);
99
+ }));
100
+ const makeTool = (runtime) => {
101
+ // SAFETY: ToolRuntime.callSchema is constructed from this tool's exact
102
+ // literal name and InputSchema before makeTool is called.
103
+ const callSchema = runtime.callSchema;
104
+ const parseCall = (input) => parseToolCall(runtime.name, callSchema, input);
105
+ const executeRuntime = (input, context) => runtime.executeServer(input,
106
+ // SAFETY: command constructors expose a required context while query
107
+ // constructors expose no context; runtime.operation owns that invariant.
108
+ unsafeCoerce(context)).pipe(Effect.flatMap((serverResult) => Effect.all({
109
+ modelResult: projectModelResult(runtime.name, runtime.modelProjection, serverResult),
110
+ views: projectViews(runtime.name, runtime.presenters, serverResult),
111
+ }).pipe(Effect.flatMap(({ modelResult, views }) => encodeToolExecutionModelContext(runtime.name, runtime.modelProjection, modelResult).pipe(Effect.map((modelContext) => ({
112
+ serverResult,
113
+ modelResult,
114
+ views,
115
+ [toolExecutionModelContext]: modelContext,
116
+ })))))), Effect.withSpan("popcomputer.structured_chat.tool.execute", {
117
+ attributes: { tool: runtime.name },
118
+ }));
119
+ // SAFETY: command constructors expose a required context while query
120
+ // constructors expose no context; both feed this operation-tagged runtime.
121
+ const execute = unsafeCoerce(executeRuntime);
122
+ const executeCallRuntime = (input, context) => parseCall(input).pipe(Effect.flatMap((call) => executeRuntime(call.arguments, context)));
123
+ return structuredDefinition("tool")({
124
+ _tag: "StructuredTool",
125
+ operation: runtime.operation,
126
+ name: runtime.name,
127
+ description: runtime.description,
128
+ inputSchema: runtime.inputSchema,
129
+ callSchema,
130
+ model: {
131
+ name: runtime.name,
132
+ description: runtime.description,
133
+ inputSchema: JSONSchema.make(runtime.inputSchema),
134
+ },
135
+ parseCall,
136
+ execute,
137
+ executeCall: unsafeCoerce(executeCallRuntime),
138
+ [toolRuntime]: runtime,
139
+ pipe() {
140
+ return Pipeable.pipeArguments(this, arguments);
141
+ },
142
+ });
143
+ };
144
+ /** Define one read-only, schema-validated structured chat tool. */
145
+ export const defineTool = (definition) => {
146
+ Schema.decodeSync(ToolNameSchema)(definition.name);
147
+ Schema.decodeSync(ToolDescriptionSchema)(definition.description);
148
+ const name = definition.name;
149
+ const description = definition.description;
150
+ const rawCallSchema = Schema.Struct({
151
+ name: Schema.Literal(name),
152
+ arguments: definition.input,
153
+ });
154
+ // SAFETY: the literal name and input schema are exactly the two ToolCall
155
+ // fields, and the constituent schemas require no runtime context.
156
+ const callSchema = unsafeCoerce(rawCallSchema);
157
+ return makeTool({
158
+ name,
159
+ description,
160
+ inputSchema: definition.input,
161
+ executeServer: (input) => definition.execute(input),
162
+ operation: "query",
163
+ modelProjection: undefined,
164
+ presenters: [],
165
+ callSchema,
166
+ });
167
+ };
168
+ /** Define one side-effecting command requiring a stable idempotency key. */
169
+ export const defineCommand = (definition) => {
170
+ Schema.decodeSync(ToolNameSchema)(definition.name);
171
+ Schema.decodeSync(ToolDescriptionSchema)(definition.description);
172
+ const rawCallSchema = Schema.Struct({
173
+ name: Schema.Literal(definition.name),
174
+ arguments: definition.input,
175
+ });
176
+ // SAFETY: the literal name and input schema exactly form ToolCall.
177
+ const callSchema = unsafeCoerce(rawCallSchema);
178
+ return makeTool({
179
+ name: definition.name,
180
+ description: definition.description,
181
+ inputSchema: definition.input,
182
+ executeServer: definition.execute,
183
+ operation: "command",
184
+ modelProjection: undefined,
185
+ presenters: [],
186
+ callSchema,
187
+ });
188
+ };
189
+ /** Add one bounded model-visible result projection to a tool. */
190
+ const modelResult = (schema, project) => (tool) => {
191
+ const runtime = tool[toolRuntime];
192
+ const nextRuntime = {
193
+ ...runtime,
194
+ modelProjection: { schema, project },
195
+ };
196
+ // SAFETY: this combinator changes only the model-projection slot from
197
+ // absent to the exact supplied schema and projector.
198
+ return makeTool(unsafeCoerce(nextRuntime));
199
+ };
200
+ /** Add one optional display-safe view projection to a tool. */
201
+ const present = (view, project) => (tool) => {
202
+ const runtime = tool[toolRuntime];
203
+ return makeTool({
204
+ ...runtime,
205
+ presenters: [
206
+ ...runtime.presenters,
207
+ { view, project },
208
+ ],
209
+ });
210
+ };
211
+ /** Combinators that add optional capabilities to a structured tool. */
212
+ export const Tool = {
213
+ modelResult,
214
+ present,
215
+ };
@@ -0,0 +1,95 @@
1
+ import { Effect, Either, Schema } from "effect";
2
+ import type * as ParseResult from "effect/ParseResult";
3
+ import type { JsonValue } from "./json-value.js";
4
+ /** Stable machine-facing name for one structured chat view. */
5
+ export declare const ViewNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
6
+ /** Positive protocol version for one structured chat view. */
7
+ export declare const ViewVersionSchema: Schema.filter<Schema.filter<typeof Schema.Number>>;
8
+ /** Minimum runtime shape retained for every structured chat view. */
9
+ export interface ViewDefinitionContract<Name extends string = string, Version extends number = number, InputSchema extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext, DataSchema extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext, PartSchema extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext> {
10
+ readonly name: Name;
11
+ readonly version: Version;
12
+ readonly inputSchema: InputSchema;
13
+ readonly dataSchema: DataSchema;
14
+ readonly partSchema: PartSchema;
15
+ /** Parse unknown application data into one display-safe data part. */
16
+ readonly parseData: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
17
+ /** Parse an unknown serialized part without requiring an Effect runtime. */
18
+ readonly decodeEither: (input: JsonValue) => Either.Either<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
19
+ }
20
+ /** Parsed data accepted when constructing one view part. */
21
+ export type ViewInput<View extends ViewDefinitionContract> = Schema.Schema.Type<View["inputSchema"]>;
22
+ /** Versioned display data carried by one view part. */
23
+ export type ViewData<View extends ViewDefinitionContract> = Schema.Schema.Type<View["dataSchema"]>;
24
+ /** Complete structured message part produced by one view. */
25
+ export type ViewPart<View extends ViewDefinitionContract> = Schema.Schema.Type<View["partSchema"]>;
26
+ /** One schema-defined, versioned structured chat view. */
27
+ export interface ViewDefinition<Name extends string, Version extends number, InputSchema extends Schema.Schema.AnyNoContext, DataSchema extends Schema.Schema.AnyNoContext, PartSchema extends Schema.Schema.AnyNoContext> extends ViewDefinitionContract<Name, Version, InputSchema, DataSchema, PartSchema> {
28
+ /** Construct and validate one display-safe data part. */
29
+ readonly make: (input: Schema.Schema.Type<InputSchema>) => Schema.Schema.Type<PartSchema>;
30
+ /** Parse unknown input into one display-safe data part. */
31
+ readonly parseData: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
32
+ /** Parse an unknown serialized part at a runtime boundary. */
33
+ readonly decode: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
34
+ /** Parse an unknown serialized part without requiring an Effect runtime. */
35
+ readonly decodeEither: (input: JsonValue) => Either.Either<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
36
+ }
37
+ /** Definition input for one versioned structured chat view. */
38
+ export interface DefineViewInput<Name extends string, Version extends number, Fields extends Schema.Struct.Fields> {
39
+ readonly name: Name;
40
+ readonly version: Version;
41
+ readonly schema: Schema.Struct<Fields>;
42
+ }
43
+ type NoContextFields<Fields extends Schema.Struct.Fields> = [
44
+ Schema.Struct.Context<Fields>
45
+ ] extends [never] ? unknown : never;
46
+ /**
47
+ * Define one typed server-to-browser view contract.
48
+ *
49
+ * The package injects `schemaVersion`; application schemas describe display
50
+ * data only. Every constructor and decoder rejects excess properties.
51
+ */
52
+ export declare const defineView: <const Name extends string, const Version extends number, Fields extends Schema.Struct.Fields>(definition: DefineViewInput<Name, Version, Fields> & NoContextFields<Fields>) => {
53
+ name: Name;
54
+ version: Version;
55
+ inputSchema: Schema.Struct<Fields>;
56
+ dataSchema: Schema.Struct<{
57
+ readonly schemaVersion: Schema.Literal<[Version]>;
58
+ } & Fields>;
59
+ partSchema: Schema.Struct<{
60
+ readonly type: Schema.Literal<["data"]>;
61
+ readonly name: Schema.Literal<[Name]>;
62
+ readonly data: Schema.Struct<{
63
+ readonly schemaVersion: Schema.Literal<[Version]>;
64
+ } & Fields>;
65
+ }>;
66
+ make: (input: Schema.Struct.Type<Fields> extends infer T ? { [K in keyof T]: T[K]; } : never) => {
67
+ readonly name: Name;
68
+ readonly type: "data";
69
+ readonly data: Schema.Struct.Type<{
70
+ readonly schemaVersion: Schema.Literal<[Version]>;
71
+ } & Fields> extends infer T_1 ? { [K_1 in keyof T_1]: T_1[K_1]; } : never;
72
+ };
73
+ parseData: (input: JsonValue) => Effect.Effect<{
74
+ readonly name: Name;
75
+ readonly type: "data";
76
+ readonly data: Schema.Struct.Type<{
77
+ readonly schemaVersion: Schema.Literal<[Version]>;
78
+ } & Fields> extends infer T ? { [K in keyof T]: T[K]; } : never;
79
+ }, ParseResult.ParseError>;
80
+ decode: (input: JsonValue) => Effect.Effect<{
81
+ readonly name: Name;
82
+ readonly type: "data";
83
+ readonly data: Schema.Struct.Type<{
84
+ readonly schemaVersion: Schema.Literal<[Version]>;
85
+ } & Fields> extends infer T ? { [K in keyof T]: T[K]; } : never;
86
+ }, ParseResult.ParseError, never>;
87
+ decodeEither: (input: JsonValue) => Either.Either<{
88
+ readonly name: Name;
89
+ readonly type: "data";
90
+ readonly data: Schema.Struct.Type<{
91
+ readonly schemaVersion: Schema.Literal<[Version]>;
92
+ } & Fields> extends infer T ? { [K in keyof T]: T[K]; } : never;
93
+ }, ParseResult.ParseError>;
94
+ };
95
+ export {};
@@ -0,0 +1,69 @@
1
+ import { Effect, Either, Schema, unsafeCoerce } from "effect";
2
+ /** Stable machine-facing name for one structured chat view. */
3
+ export const ViewNameSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100), Schema.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
4
+ /** Positive protocol version for one structured chat view. */
5
+ export const ViewVersionSchema = Schema.Number.pipe(Schema.int(), Schema.between(1, 2_147_483_647));
6
+ /**
7
+ * Define one typed server-to-browser view contract.
8
+ *
9
+ * The package injects `schemaVersion`; application schemas describe display
10
+ * data only. Every constructor and decoder rejects excess properties.
11
+ */
12
+ export const defineView = (definition) => {
13
+ Schema.decodeSync(ViewNameSchema)(definition.name);
14
+ Schema.decodeSync(ViewVersionSchema)(definition.version);
15
+ if ("schemaVersion" in definition.schema.fields) {
16
+ throw new Error("View schemas cannot define the reserved schemaVersion field");
17
+ }
18
+ // SAFETY: the reserved schemaVersion field cannot be supplied by Fields;
19
+ // the literal therefore augments the exact application schema once.
20
+ const dataSchema = Schema.Struct({
21
+ schemaVersion: Schema.Literal(definition.version),
22
+ ...definition.schema.fields,
23
+ });
24
+ // SAFETY: these literals and dataSchema exactly describe ViewPart<Name,
25
+ // Version, Fields>; the assertions retain that generic correlation.
26
+ const partSchema = Schema.Struct({
27
+ type: Schema.Literal("data"),
28
+ name: Schema.Literal(definition.name),
29
+ data: dataSchema,
30
+ });
31
+ // SAFETY: NoContextFields excludes schemas with runtime requirements; this
32
+ // assertion preserves definition.schema's existing Type and Encoded sides.
33
+ const runtimeInputSchema = unsafeCoerce(definition.schema);
34
+ // SAFETY: partSchema was built immediately above from the exact view name,
35
+ // version, and application fields, with no runtime schema requirements.
36
+ const runtimePartSchema = unsafeCoerce(partSchema);
37
+ const decodePart = Schema.decodeUnknown(runtimePartSchema);
38
+ const decodePartEither = Schema.decodeUnknownEither(runtimePartSchema);
39
+ const validatePart = Schema.validate(runtimePartSchema);
40
+ const parseData = (input) => Schema.validate(runtimeInputSchema)(input, {
41
+ onExcessProperty: "error",
42
+ }).pipe(Effect.flatMap((data) => validatePart({
43
+ type: "data",
44
+ name: definition.name,
45
+ data: {
46
+ schemaVersion: definition.version,
47
+ ...data,
48
+ },
49
+ }, { onExcessProperty: "error" })));
50
+ const make = (input) => Schema.validateSync(runtimePartSchema)({
51
+ type: "data",
52
+ name: definition.name,
53
+ data: {
54
+ schemaVersion: definition.version,
55
+ ...input,
56
+ },
57
+ }, { onExcessProperty: "error" });
58
+ return {
59
+ name: definition.name,
60
+ version: definition.version,
61
+ inputSchema: definition.schema,
62
+ dataSchema,
63
+ partSchema,
64
+ make,
65
+ parseData,
66
+ decode: (input) => decodePart(input, { onExcessProperty: "error" }),
67
+ decodeEither: (input) => decodePartEither(input, { onExcessProperty: "error" }),
68
+ };
69
+ };