@popcomputer/structured-chat 0.1.0 → 0.2.0-rc.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 (41) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +122 -108
  3. package/dist/adapters/openai-compatible-model.d.ts +4 -4
  4. package/dist/adapters/openai-compatible-model.js +33 -31
  5. package/dist/core/answer.d.ts +13 -13
  6. package/dist/core/answer.js +21 -12
  7. package/dist/core/chat.d.ts +12 -15
  8. package/dist/core/chat.js +36 -27
  9. package/dist/core/collect-stage.d.ts +11 -14
  10. package/dist/core/collect-stage.js +40 -37
  11. package/dist/core/command.d.ts +1 -1
  12. package/dist/core/command.js +1 -1
  13. package/dist/core/json-value.d.ts +1 -1
  14. package/dist/core/json-value.js +8 -1
  15. package/dist/core/model-guard.d.ts +2 -3
  16. package/dist/core/model-guard.js +4 -4
  17. package/dist/core/model.d.ts +15 -19
  18. package/dist/core/model.js +22 -10
  19. package/dist/core/protocol.d.ts +84 -79
  20. package/dist/core/protocol.js +18 -12
  21. package/dist/core/question.js +17 -15
  22. package/dist/core/repair.js +1 -1
  23. package/dist/core/session.d.ts +22 -28
  24. package/dist/core/session.js +16 -7
  25. package/dist/core/stage-name.d.ts +1 -1
  26. package/dist/core/stage-name.js +1 -1
  27. package/dist/core/stage.d.ts +4 -4
  28. package/dist/core/stage.js +11 -8
  29. package/dist/core/tool-set.js +6 -6
  30. package/dist/core/tool.d.ts +36 -39
  31. package/dist/core/tool.js +58 -31
  32. package/dist/core/view.d.ts +64 -39
  33. package/dist/core/view.js +12 -15
  34. package/dist/integrations/assistant-ui.js +21 -14
  35. package/dist/testing/in-memory-session-store.js +1 -1
  36. package/dist/testing/scenario.js +6 -6
  37. package/examples/answer-modes.ts +60 -49
  38. package/examples/prompt-injection-policy.ts +20 -20
  39. package/examples/resource-search.ts +104 -0
  40. package/package.json +11 -3
  41. package/examples/agency-search.ts +0 -101
package/dist/core/tool.js CHANGED
@@ -1,21 +1,35 @@
1
- import { Effect, JSONSchema, Pipeable, Schema, unsafeCoerce, } from "effect";
2
- import * as ParseResult from "effect/ParseResult";
1
+ import { Effect, Function as Fn, JsonSchema, Pipeable, Result, Schema, SchemaIssue, } from "effect";
3
2
  import { structuredDefinition, } from "./definition.js";
4
3
  /** 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]+)*$/));
4
+ export const ToolNameSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100), Schema.isPattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
6
5
  /** Bounded model-facing description for one structured chat tool. */
7
- export const ToolDescriptionSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
6
+ export const ToolDescriptionSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
8
7
  /** Safe reason that a model-authored tool call was rejected. */
9
- export const InvalidToolCallReasonSchema = Schema.Literal("invalid_envelope", "unknown_tool", "invalid_arguments");
8
+ export const InvalidToolCallReasonSchema = Schema.Literals([
9
+ "invalid_envelope",
10
+ "unknown_tool",
11
+ "invalid_arguments",
12
+ ]);
10
13
  /** A model-authored tool call failed strict parsing. */
11
14
  export class InvalidToolCall extends Schema.TaggedError()("InvalidToolCall", {
12
15
  tool: Schema.NullOr(ToolNameSchema),
13
16
  reason: InvalidToolCallReasonSchema,
14
- path: Schema.NullOr(Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500))),
17
+ path: Schema.NullOr(Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(500))),
15
18
  }) {
16
19
  }
17
20
  /** Safe reason that an application-owned tool projection was rejected. */
18
- export const InvalidToolProjectionReasonSchema = Schema.Literal("invalid_model_result", "invalid_view_data");
21
+ export const InvalidToolProjectionReasonSchema = Schema.Literals([
22
+ "invalid_model_result",
23
+ "invalid_view_data",
24
+ ]);
25
+ const StandardSchemaPathSegment = Schema.Struct({
26
+ key: Schema.Union([
27
+ Schema.String,
28
+ Schema.Number,
29
+ Schema.Symbol,
30
+ ]),
31
+ });
32
+ const decodeStandardSchemaPathSegment = Schema.decodeUnknownResult(StandardSchemaPathSegment);
19
33
  /** An application-owned model or view projection violated its schema. */
20
34
  export class InvalidToolProjection extends Schema.TaggedError()("InvalidToolProjection", {
21
35
  tool: ToolNameSchema,
@@ -25,21 +39,23 @@ export class InvalidToolProjection extends Schema.TaggedError()("InvalidToolProj
25
39
  }
26
40
  const toolRuntime = Symbol("@popcomputer/structured-chat/ToolRuntime");
27
41
  const toolExecutionModelContext = Symbol("@popcomputer/structured-chat/ToolExecutionModelContext");
28
- const ToolExecutionModelContextSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(40_000));
42
+ const ToolExecutionModelContextSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(40_000));
29
43
  /** @internal Read the bounded model-visible context retained by one execution. */
30
44
  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];
45
+ return execution[toolExecutionModelContext];
35
46
  };
36
- const parseToolCall = (name, callSchema, input) => Schema.decodeUnknown(callSchema)(input, {
47
+ const parseToolCall = (name, callSchema, input) => Schema.decodeUnknownEffect(callSchema)(input, {
37
48
  onExcessProperty: "error",
38
49
  }).pipe(Effect.mapError((error) => {
39
- const issue = ParseResult.ArrayFormatter.formatErrorSync(error)[0];
40
- const issuePath = issue === undefined || issue.path.length === 0
50
+ const issue = SchemaIssue.makeFormatterStandardSchemaV1()(error.issue)
51
+ .issues[0];
52
+ const path = issue?.path ?? [];
53
+ const issuePath = path.length === 0
41
54
  ? "#"
42
- : `#/${issue.path.map(String).join("/")}`;
55
+ : `#/${path.map((segment) => Result.match(decodeStandardSchemaPathSegment(segment), {
56
+ onFailure: () => String(segment),
57
+ onSuccess: ({ key }) => String(key),
58
+ })).join("/")}`;
43
59
  return new InvalidToolCall({
44
60
  tool: name,
45
61
  reason: "invalid_arguments",
@@ -50,7 +66,7 @@ const projectModelResult = (tool, projection, result) => {
50
66
  if (projection === undefined) {
51
67
  // SAFETY: The conditional result type is exactly undefined when no model
52
68
  // projection schema is configured.
53
- return Effect.succeed(undefined);
69
+ return Effect.succeed(Fn.cast(undefined));
54
70
  }
55
71
  const invalidProjection = () => new InvalidToolProjection({
56
72
  tool,
@@ -60,21 +76,21 @@ const projectModelResult = (tool, projection, result) => {
60
76
  return Effect.try({
61
77
  try: () => projection.project(result),
62
78
  catch: invalidProjection,
63
- }).pipe(Effect.flatMap((projected) => Schema.validate(projection.schema)(projected, {
79
+ }).pipe(Effect.flatMap((projected) => Schema.decodeEffect(Schema.toType(projection.schema))(projected, {
64
80
  onExcessProperty: "error",
65
81
  })), Effect.mapError(invalidProjection),
66
- // SAFETY: Schema.validate returned the configured model schema's exact
82
+ // SAFETY: decoding the Type side returned the configured schema's exact
67
83
  // Type side, which is ToolModelResult<ModelSchema> in this branch.
68
- Effect.map((value) => value));
84
+ Effect.map((value) => Fn.cast(value)));
69
85
  };
70
86
  const encodeToolExecutionModelContext = (tool, projection, modelResult) => {
71
87
  if (projection === undefined) {
72
88
  return Effect.succeed(undefined);
73
89
  }
74
- return Schema.encodeUnknown(projection.schema)(modelResult).pipe(Effect.flatMap((encodedResult) => Schema.encodeUnknown(Schema.parseJson())({
90
+ return Schema.encodeUnknownEffect(projection.schema)(modelResult).pipe(Effect.flatMap((encodedResult) => Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))({
75
91
  tool,
76
92
  result: encodedResult,
77
- })), Effect.flatMap((context) => Schema.decodeUnknown(ToolExecutionModelContextSchema)(context)), Effect.mapError(() => new InvalidToolProjection({
93
+ })), Effect.flatMap((context) => Schema.decodeUnknownEffect(ToolExecutionModelContextSchema)(context)), Effect.mapError(() => new InvalidToolProjection({
78
94
  tool,
79
95
  target: "model_context",
80
96
  reason: "invalid_model_result",
@@ -95,17 +111,28 @@ const projectViews = (tool, presenters, result) => Effect.forEach(presenters, (p
95
111
  }).pipe(Effect.map((parts) => parts.flatMap((part) => part === undefined ? [] : [part])), Effect.map((parts) => {
96
112
  // SAFETY: Every retained part was parsed by the corresponding presenter
97
113
  // view, and the output union is derived from that same presenter tuple.
98
- return unsafeCoerce(parts);
114
+ return Fn.cast(parts);
99
115
  }));
116
+ const makeModelInputSchema = (schema) => {
117
+ const document = JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema));
118
+ const definitions = Object.keys(document.definitions).length === 0
119
+ ? {}
120
+ : { definitions: document.definitions };
121
+ return {
122
+ $schema: JsonSchema.META_SCHEMA_URI_DRAFT_07,
123
+ ...document.schema,
124
+ ...definitions,
125
+ };
126
+ };
100
127
  const makeTool = (runtime) => {
101
128
  // SAFETY: ToolRuntime.callSchema is constructed from this tool's exact
102
129
  // literal name and InputSchema before makeTool is called.
103
- const callSchema = runtime.callSchema;
130
+ const callSchema = Fn.cast(runtime.callSchema);
104
131
  const parseCall = (input) => parseToolCall(runtime.name, callSchema, input);
105
132
  const executeRuntime = (input, context) => runtime.executeServer(input,
106
133
  // SAFETY: command constructors expose a required context while query
107
134
  // constructors expose no context; runtime.operation owns that invariant.
108
- unsafeCoerce(context)).pipe(Effect.flatMap((serverResult) => Effect.all({
135
+ Fn.cast(context)).pipe(Effect.flatMap((serverResult) => Effect.all({
109
136
  modelResult: projectModelResult(runtime.name, runtime.modelProjection, serverResult),
110
137
  views: projectViews(runtime.name, runtime.presenters, serverResult),
111
138
  }).pipe(Effect.flatMap(({ modelResult, views }) => encodeToolExecutionModelContext(runtime.name, runtime.modelProjection, modelResult).pipe(Effect.map((modelContext) => ({
@@ -118,7 +145,7 @@ const makeTool = (runtime) => {
118
145
  }));
119
146
  // SAFETY: command constructors expose a required context while query
120
147
  // constructors expose no context; both feed this operation-tagged runtime.
121
- const execute = unsafeCoerce(executeRuntime);
148
+ const execute = Fn.cast(executeRuntime);
122
149
  const executeCallRuntime = (input, context) => parseCall(input).pipe(Effect.flatMap((call) => executeRuntime(call.arguments, context)));
123
150
  return structuredDefinition("tool")({
124
151
  _tag: "StructuredTool",
@@ -130,11 +157,11 @@ const makeTool = (runtime) => {
130
157
  model: {
131
158
  name: runtime.name,
132
159
  description: runtime.description,
133
- inputSchema: JSONSchema.make(runtime.inputSchema),
160
+ inputSchema: makeModelInputSchema(runtime.inputSchema),
134
161
  },
135
162
  parseCall,
136
163
  execute,
137
- executeCall: unsafeCoerce(executeCallRuntime),
164
+ executeCall: Fn.cast(executeCallRuntime),
138
165
  [toolRuntime]: runtime,
139
166
  pipe() {
140
167
  return Pipeable.pipeArguments(this, arguments);
@@ -153,7 +180,7 @@ export const defineTool = (definition) => {
153
180
  });
154
181
  // SAFETY: the literal name and input schema are exactly the two ToolCall
155
182
  // fields, and the constituent schemas require no runtime context.
156
- const callSchema = unsafeCoerce(rawCallSchema);
183
+ const callSchema = Fn.cast(rawCallSchema);
157
184
  return makeTool({
158
185
  name,
159
186
  description,
@@ -174,7 +201,7 @@ export const defineCommand = (definition) => {
174
201
  arguments: definition.input,
175
202
  });
176
203
  // SAFETY: the literal name and input schema exactly form ToolCall.
177
- const callSchema = unsafeCoerce(rawCallSchema);
204
+ const callSchema = Fn.cast(rawCallSchema);
178
205
  return makeTool({
179
206
  name: definition.name,
180
207
  description: definition.description,
@@ -195,7 +222,7 @@ const modelResult = (schema, project) => (tool) => {
195
222
  };
196
223
  // SAFETY: this combinator changes only the model-projection slot from
197
224
  // absent to the exact supplied schema and projector.
198
- return makeTool(unsafeCoerce(nextRuntime));
225
+ return makeTool(Fn.cast(nextRuntime));
199
226
  };
200
227
  /** Add one optional display-safe view projection to a tool. */
201
228
  const present = (view, project) => (tool) => {
@@ -1,38 +1,47 @@
1
- import { Effect, Either, Schema } from "effect";
2
- import type * as ParseResult from "effect/ParseResult";
1
+ import { Effect, Result, Schema } from "effect";
3
2
  import type { JsonValue } from "./json-value.js";
4
3
  /** Stable machine-facing name for one structured chat view. */
5
- export declare const ViewNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
4
+ export declare const ViewNameSchema: Schema.Trimmed;
6
5
  /** Positive protocol version for one structured chat view. */
7
- export declare const ViewVersionSchema: Schema.filter<Schema.filter<typeof Schema.Number>>;
6
+ export declare const ViewVersionSchema: Schema.Number;
7
+ /** Schema accepted at view boundaries without runtime services. */
8
+ export type ViewSchema = Schema.Codec<unknown, unknown, never, never>;
9
+ /** Schema for one decoded view part correlated with its view data. */
10
+ export type ViewPartSchema<Name extends string = string, DataSchema extends ViewSchema = ViewSchema> = Schema.Codec<{
11
+ readonly type: "data";
12
+ readonly name: Name;
13
+ readonly data: Schema.Schema.Type<DataSchema>;
14
+ }, unknown, never, never>;
8
15
  /** 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> {
16
+ export interface ViewDefinitionContract<Name extends string = string, Version extends number = number, InputSchema extends ViewSchema = ViewSchema, DataSchema extends ViewSchema = ViewSchema, PartSchema extends ViewPartSchema<Name, DataSchema> = ViewPartSchema<Name, DataSchema>> {
10
17
  readonly name: Name;
11
18
  readonly version: Version;
12
19
  readonly inputSchema: InputSchema;
13
20
  readonly dataSchema: DataSchema;
14
21
  readonly partSchema: PartSchema;
15
22
  /** Parse unknown application data into one display-safe data part. */
16
- readonly parseData: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
23
+ parseData(input: ViewInput<this>): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
17
24
  /** Parse an unknown serialized part without requiring an Effect runtime. */
18
- readonly decodeEither: (input: JsonValue) => Either.Either<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
25
+ decodeResult(input: JsonValue): Result.Result<ViewPart<this>, Schema.SchemaError>;
19
26
  }
20
27
  /** Parsed data accepted when constructing one view part. */
21
28
  export type ViewInput<View extends ViewDefinitionContract> = Schema.Schema.Type<View["inputSchema"]>;
22
29
  /** Versioned display data carried by one view part. */
23
30
  export type ViewData<View extends ViewDefinitionContract> = Schema.Schema.Type<View["dataSchema"]>;
24
31
  /** Complete structured message part produced by one view. */
25
- export type ViewPart<View extends ViewDefinitionContract> = Schema.Schema.Type<View["partSchema"]>;
32
+ export type ViewPart<View extends ViewDefinitionContract> = Schema.Schema.Type<View["partSchema"]> & {
33
+ readonly data: ViewData<View>;
34
+ };
26
35
  /** 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> {
36
+ export interface ViewDefinition<Name extends string, Version extends number, InputSchema extends ViewSchema, DataSchema extends ViewSchema, PartSchema extends ViewPartSchema<Name, DataSchema>> extends ViewDefinitionContract<Name, Version, InputSchema, DataSchema, PartSchema> {
28
37
  /** Construct and validate one display-safe data part. */
29
- readonly make: (input: Schema.Schema.Type<InputSchema>) => Schema.Schema.Type<PartSchema>;
38
+ make(input: Schema.Schema.Type<InputSchema>): Schema.Schema.Type<PartSchema>;
30
39
  /** Parse unknown input into one display-safe data part. */
31
- readonly parseData: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
40
+ parseData(input: ViewInput<this>): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
32
41
  /** Parse an unknown serialized part at a runtime boundary. */
33
- readonly decode: (input: JsonValue) => Effect.Effect<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
42
+ decode(input: JsonValue): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
34
43
  /** Parse an unknown serialized part without requiring an Effect runtime. */
35
- readonly decodeEither: (input: JsonValue) => Either.Either<Schema.Schema.Type<PartSchema>, ParseResult.ParseError>;
44
+ decodeResult(input: JsonValue): Result.Result<ViewPart<this>, Schema.SchemaError>;
36
45
  }
37
46
  /** Definition input for one versioned structured chat view. */
38
47
  export interface DefineViewInput<Name extends string, Version extends number, Fields extends Schema.Struct.Fields> {
@@ -41,7 +50,7 @@ export interface DefineViewInput<Name extends string, Version extends number, Fi
41
50
  readonly schema: Schema.Struct<Fields>;
42
51
  }
43
52
  type NoContextFields<Fields extends Schema.Struct.Fields> = [
44
- Schema.Struct.Context<Fields>
53
+ Schema.Struct.DecodingServices<Fields> | Schema.Struct.EncodingServices<Fields>
45
54
  ] extends [never] ? unknown : never;
46
55
  /**
47
56
  * Define one typed server-to-browser view contract.
@@ -54,42 +63,58 @@ export declare const defineView: <const Name extends string, const Version exten
54
63
  version: Version;
55
64
  inputSchema: Schema.Struct<Fields>;
56
65
  dataSchema: Schema.Struct<{
57
- readonly schemaVersion: Schema.Literal<[Version]>;
66
+ readonly schemaVersion: Schema.Literal<Version>;
58
67
  } & Fields>;
59
68
  partSchema: Schema.Struct<{
60
- readonly type: Schema.Literal<["data"]>;
61
- readonly name: Schema.Literal<[Name]>;
69
+ readonly type: Schema.Literal<"data">;
70
+ readonly name: Schema.Literal<Name>;
62
71
  readonly data: Schema.Struct<{
63
- readonly schemaVersion: Schema.Literal<[Version]>;
72
+ readonly schemaVersion: Schema.Literal<Version>;
64
73
  } & Fields>;
65
74
  }>;
66
- make: (input: Schema.Struct.Type<Fields> extends infer T ? { [K in keyof T]: T[K]; } : never) => {
67
- readonly name: Name;
75
+ make: (input: Schema.Struct.View<Fields, "Type", Schema.Struct.TypeOptionalKeys<Fields>, Schema.Struct.TypeMutableKeys<Fields>>) => {
68
76
  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
77
  readonly name: Name;
78
+ readonly data: Schema.Struct.View<{
79
+ readonly schemaVersion: Schema.Literal<Version>;
80
+ } & Fields, "Type", Schema.Struct.TypeOptionalKeys<{
81
+ readonly schemaVersion: Schema.Literal<Version>;
82
+ } & Fields>, Schema.Struct.TypeMutableKeys<{
83
+ readonly schemaVersion: Schema.Literal<Version>;
84
+ } & Fields>>;
85
+ };
86
+ parseData: (input: Schema.Struct.View<Fields, "Type", Schema.Struct.TypeOptionalKeys<Fields>, Schema.Struct.TypeMutableKeys<Fields>>) => Effect.Effect<{
75
87
  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
88
  readonly name: Name;
89
+ readonly data: Schema.Struct.View<{
90
+ readonly schemaVersion: Schema.Literal<Version>;
91
+ } & Fields, "Type", Schema.Struct.TypeOptionalKeys<{
92
+ readonly schemaVersion: Schema.Literal<Version>;
93
+ } & Fields>, Schema.Struct.TypeMutableKeys<{
94
+ readonly schemaVersion: Schema.Literal<Version>;
95
+ } & Fields>>;
96
+ }, Schema.SchemaError>;
97
+ decode: (input: JsonValue) => Effect.Effect<{
82
98
  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
99
  readonly name: Name;
100
+ readonly data: Schema.Struct.View<{
101
+ readonly schemaVersion: Schema.Literal<Version>;
102
+ } & Fields, "Type", Schema.Struct.TypeOptionalKeys<{
103
+ readonly schemaVersion: Schema.Literal<Version>;
104
+ } & Fields>, Schema.Struct.TypeMutableKeys<{
105
+ readonly schemaVersion: Schema.Literal<Version>;
106
+ } & Fields>>;
107
+ }, Schema.SchemaError, never>;
108
+ decodeResult: (input: JsonValue) => Result.Result<{
89
109
  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>;
110
+ readonly name: Name;
111
+ readonly data: Schema.Struct.View<{
112
+ readonly schemaVersion: Schema.Literal<Version>;
113
+ } & Fields, "Type", Schema.Struct.TypeOptionalKeys<{
114
+ readonly schemaVersion: Schema.Literal<Version>;
115
+ } & Fields>, Schema.Struct.TypeMutableKeys<{
116
+ readonly schemaVersion: Schema.Literal<Version>;
117
+ } & Fields>>;
118
+ }, Schema.SchemaError>;
94
119
  };
95
120
  export {};
package/dist/core/view.js CHANGED
@@ -1,8 +1,8 @@
1
- import { Effect, Either, Schema, unsafeCoerce } from "effect";
1
+ import { Effect, Function as Fn, Result, Schema } from "effect";
2
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]+)*$/));
3
+ export const ViewNameSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100), Schema.isPattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
4
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));
5
+ export const ViewVersionSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 2_147_483_647 }));
6
6
  /**
7
7
  * Define one typed server-to-browser view contract.
8
8
  *
@@ -15,14 +15,10 @@ export const defineView = (definition) => {
15
15
  if ("schemaVersion" in definition.schema.fields) {
16
16
  throw new Error("View schemas cannot define the reserved schemaVersion field");
17
17
  }
18
- // SAFETY: the reserved schemaVersion field cannot be supplied by Fields;
19
- // the literal therefore augments the exact application schema once.
20
18
  const dataSchema = Schema.Struct({
21
19
  schemaVersion: Schema.Literal(definition.version),
22
20
  ...definition.schema.fields,
23
21
  });
24
- // SAFETY: these literals and dataSchema exactly describe ViewPart<Name,
25
- // Version, Fields>; the assertions retain that generic correlation.
26
22
  const partSchema = Schema.Struct({
27
23
  type: Schema.Literal("data"),
28
24
  name: Schema.Literal(definition.name),
@@ -30,14 +26,15 @@ export const defineView = (definition) => {
30
26
  });
31
27
  // SAFETY: NoContextFields excludes schemas with runtime requirements; this
32
28
  // assertion preserves definition.schema's existing Type and Encoded sides.
33
- const runtimeInputSchema = unsafeCoerce(definition.schema);
29
+ const runtimeInputSchema = Fn.cast(definition.schema);
34
30
  // SAFETY: partSchema was built immediately above from the exact view name,
35
31
  // 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, {
32
+ const runtimePartSchema = Fn.cast(partSchema);
33
+ const decodePart = Schema.decodeUnknownEffect(runtimePartSchema);
34
+ const decodePartResult = Schema.decodeUnknownResult(runtimePartSchema);
35
+ const validateInput = Schema.decodeUnknownEffect(Schema.toType(runtimeInputSchema));
36
+ const validatePart = Schema.decodeUnknownEffect(Schema.toType(runtimePartSchema));
37
+ const parseData = (input) => validateInput(input, {
41
38
  onExcessProperty: "error",
42
39
  }).pipe(Effect.flatMap((data) => validatePart({
43
40
  type: "data",
@@ -47,7 +44,7 @@ export const defineView = (definition) => {
47
44
  ...data,
48
45
  },
49
46
  }, { onExcessProperty: "error" })));
50
- const make = (input) => Schema.validateSync(runtimePartSchema)({
47
+ const make = (input) => Schema.decodeUnknownSync(Schema.toType(runtimePartSchema))({
51
48
  type: "data",
52
49
  name: definition.name,
53
50
  data: {
@@ -64,6 +61,6 @@ export const defineView = (definition) => {
64
61
  make,
65
62
  parseData,
66
63
  decode: (input) => decodePart(input, { onExcessProperty: "error" }),
67
- decodeEither: (input) => decodePartEither(input, { onExcessProperty: "error" }),
64
+ decodeResult: (input) => decodePartResult(input, { onExcessProperty: "error" }),
68
65
  };
69
66
  };
@@ -1,7 +1,8 @@
1
1
  import { makeAssistantDataUI, } from "@assistant-ui/core/react";
2
- import { Either, Schema } from "effect";
2
+ import { Exit, Result, Schema } from "effect";
3
3
  import { createElement } from "react";
4
4
  import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, } from "../core/protocol.js";
5
+ import { JsonValueSchema } from "../core/json-value.js";
5
6
  /**
6
7
  * Register one defineView contract as a strictly decoded assistant-ui data UI.
7
8
  *
@@ -10,19 +11,25 @@ import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema,
10
11
  */
11
12
  export const makeAssistantView = (view, config) => {
12
13
  const Renderer = (props) => {
13
- const decoded = Schema.decodeUnknownEither(view.partSchema)({
14
+ const data = Schema.decodeUnknownResult(JsonValueSchema)(props.data);
15
+ if (Result.isFailure(data)) {
16
+ return config.fallback === undefined
17
+ ? null
18
+ : createElement(config.fallback, props);
19
+ }
20
+ const decoded = view.decodeResult({
14
21
  type: "data",
15
22
  name: view.name,
16
- data: props.data,
17
- }, { onExcessProperty: "error" });
18
- if (Either.isLeft(decoded)) {
23
+ data: data.success,
24
+ });
25
+ if (Result.isFailure(decoded)) {
19
26
  return config.fallback === undefined
20
27
  ? null
21
28
  : createElement(config.fallback, props);
22
29
  }
23
30
  return createElement(config.render, {
24
31
  ...props,
25
- data: decoded.right.data,
32
+ data: decoded.success.data,
26
33
  });
27
34
  };
28
35
  return makeAssistantDataUI({
@@ -42,9 +49,9 @@ const readLatestSession = (messages) => {
42
49
  if (message?.role !== "assistant") {
43
50
  continue;
44
51
  }
45
- const session = Schema.decodeUnknownEither(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
46
- if (Either.isRight(session)) {
47
- return session.right;
52
+ const session = Schema.decodeUnknownExit(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
53
+ if (Exit.isSuccess(session)) {
54
+ return session.value;
48
55
  }
49
56
  }
50
57
  return undefined;
@@ -95,17 +102,17 @@ export const makeAssistantChatModelAdapter = (options) => {
95
102
  catch {
96
103
  throw new Error("Structured chat returned an invalid response");
97
104
  }
98
- const decoded = Schema.decodeUnknownEither(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
99
- if (Either.isLeft(decoded)) {
105
+ const decoded = Schema.decodeUnknownExit(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
106
+ if (Exit.isFailure(decoded)) {
100
107
  throw new Error("Structured chat returned an invalid response");
101
108
  }
102
109
  return {
103
- content: decoded.right.message.content,
110
+ content: decoded.value.message.content,
104
111
  metadata: {
105
- custom: decoded.right.session === undefined
112
+ custom: decoded.value.session === undefined
106
113
  ? {}
107
114
  : {
108
- [assistantChatSessionMetadataKey]: decoded.right.session,
115
+ [assistantChatSessionMetadataKey]: decoded.value.session,
109
116
  },
110
117
  },
111
118
  };
@@ -30,7 +30,7 @@ const replaceSnapshot = (sessions, input) => {
30
30
  return [snapshot, next];
31
31
  };
32
32
  /** In-memory optimistic session store intended for tests and examples. */
33
- export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ({
33
+ export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ChatSessionStore.of({
34
34
  load: (scope) => Ref.get(sessions).pipe(Effect.map((current) => current.get(scopeKey(scope)) ?? null)),
35
35
  replace: (input) => Ref.modify(sessions, (current) => replaceSnapshot(current, input)).pipe(Effect.flatMap((result) => result instanceof ChatSessionConflict
36
36
  ? Effect.fail(result)
@@ -2,7 +2,7 @@ import { Effect, Layer, Ref, Schema } from "effect";
2
2
  import { StructuredChatModel, } from "../core/model.js";
3
3
  import { JsonValueSchema, } from "../core/json-value.js";
4
4
  const scenarioQuote = Symbol("@popcomputer/structured-chat/testing/ScenarioQuote");
5
- const ScenarioQuoteSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
5
+ const ScenarioQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
6
6
  const evidenceIndex = (request, quoted) => {
7
7
  if (quoted.messageIndex !== undefined) {
8
8
  const message = request.untrustedMessages[quoted.messageIndex];
@@ -49,7 +49,7 @@ const answers = (stage, proposed, options = {}) => ({
49
49
  if (value === undefined || answer === undefined) {
50
50
  continue;
51
51
  }
52
- encodedAnswers[field] = Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
52
+ encodedAnswers[field] = Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
53
53
  evidenceIndex(request, value);
54
54
  evidence.push({
55
55
  field,
@@ -76,7 +76,7 @@ const answers = (stage, proposed, options = {}) => ({
76
76
  const call = (tool, input) => ({
77
77
  respond: () => ({
78
78
  name: tool.name,
79
- arguments: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
79
+ arguments: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
80
80
  }),
81
81
  });
82
82
  const replace = (stage, field, value, options) => {
@@ -92,7 +92,7 @@ const replace = (stage, field, value, options) => {
92
92
  _tag: "ReplaceAcceptedAnswer",
93
93
  stage: stage.name,
94
94
  field,
95
- value: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
95
+ value: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
96
96
  evidence: {
97
97
  quote: support.quote,
98
98
  },
@@ -126,14 +126,14 @@ const repairs = (first, ...remaining) => ({
126
126
  });
127
127
  const model = (first, ...remaining) => Layer.effect(StructuredChatModel, Ref.make(0).pipe(Effect.map((cursor) => {
128
128
  const steps = [first, ...remaining];
129
- return {
129
+ return StructuredChatModel.of({
130
130
  requestTool: (request) => Ref.getAndUpdate(cursor, (index) => index + 1).pipe(Effect.flatMap((index) => {
131
131
  const step = steps[index];
132
132
  return step === undefined
133
133
  ? Effect.die(new Error(`Scenario model exhausted after ${steps.length} requests`))
134
134
  : Effect.sync(() => step.respond(request));
135
135
  })),
136
- };
136
+ });
137
137
  })));
138
138
  /** Typed constructors for concise valid transcript scenarios. */
139
139
  export const Scenario = {