@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.
- package/LICENSE +0 -1
- package/README.md +122 -108
- package/dist/adapters/openai-compatible-model.d.ts +4 -4
- package/dist/adapters/openai-compatible-model.js +33 -31
- package/dist/core/answer.d.ts +13 -13
- package/dist/core/answer.js +21 -12
- package/dist/core/chat.d.ts +12 -15
- package/dist/core/chat.js +36 -27
- package/dist/core/collect-stage.d.ts +11 -14
- package/dist/core/collect-stage.js +40 -37
- package/dist/core/command.d.ts +1 -1
- package/dist/core/command.js +1 -1
- package/dist/core/json-value.d.ts +1 -1
- package/dist/core/json-value.js +8 -1
- package/dist/core/model-guard.d.ts +2 -3
- package/dist/core/model-guard.js +4 -4
- package/dist/core/model.d.ts +15 -19
- package/dist/core/model.js +22 -10
- package/dist/core/protocol.d.ts +84 -79
- package/dist/core/protocol.js +18 -12
- package/dist/core/question.js +17 -15
- package/dist/core/repair.js +1 -1
- package/dist/core/session.d.ts +22 -28
- package/dist/core/session.js +16 -7
- package/dist/core/stage-name.d.ts +1 -1
- package/dist/core/stage-name.js +1 -1
- package/dist/core/stage.d.ts +4 -4
- package/dist/core/stage.js +11 -8
- package/dist/core/tool-set.js +6 -6
- package/dist/core/tool.d.ts +36 -39
- package/dist/core/tool.js +58 -31
- package/dist/core/view.d.ts +64 -39
- package/dist/core/view.js +12 -15
- package/dist/integrations/assistant-ui.js +21 -14
- package/dist/testing/in-memory-session-store.js +1 -1
- package/dist/testing/scenario.js +6 -6
- package/examples/answer-modes.ts +60 -49
- package/examples/prompt-injection-policy.ts +20 -20
- package/examples/resource-search.ts +104 -0
- package/package.json +11 -3
- package/examples/agency-search.ts +0 -101
package/dist/core/tool.js
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
import { 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
47
|
+
const parseToolCall = (name, callSchema, input) => Schema.decodeUnknownEffect(callSchema)(input, {
|
|
37
48
|
onExcessProperty: "error",
|
|
38
49
|
}).pipe(Effect.mapError((error) => {
|
|
39
|
-
const issue =
|
|
40
|
-
|
|
50
|
+
const issue = SchemaIssue.makeFormatterStandardSchemaV1()(error.issue)
|
|
51
|
+
.issues[0];
|
|
52
|
+
const path = issue?.path ?? [];
|
|
53
|
+
const issuePath = path.length === 0
|
|
41
54
|
? "#"
|
|
42
|
-
: `#/${
|
|
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.
|
|
79
|
+
}).pipe(Effect.flatMap((projected) => Schema.decodeEffect(Schema.toType(projection.schema))(projected, {
|
|
64
80
|
onExcessProperty: "error",
|
|
65
81
|
})), Effect.mapError(invalidProjection),
|
|
66
|
-
// SAFETY:
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
160
|
+
inputSchema: makeModelInputSchema(runtime.inputSchema),
|
|
134
161
|
},
|
|
135
162
|
parseCall,
|
|
136
163
|
execute,
|
|
137
|
-
executeCall:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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) => {
|
package/dist/core/view.d.ts
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import { 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.
|
|
4
|
+
export declare const ViewNameSchema: Schema.Trimmed;
|
|
6
5
|
/** Positive protocol version for one structured chat view. */
|
|
7
|
-
export declare const ViewVersionSchema: Schema.
|
|
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
|
|
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
|
-
|
|
23
|
+
parseData(input: ViewInput<this>): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
|
|
17
24
|
/** Parse an unknown serialized part without requiring an Effect runtime. */
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
38
|
+
make(input: Schema.Schema.Type<InputSchema>): Schema.Schema.Type<PartSchema>;
|
|
30
39
|
/** Parse unknown input into one display-safe data part. */
|
|
31
|
-
|
|
40
|
+
parseData(input: ViewInput<this>): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
|
|
32
41
|
/** Parse an unknown serialized part at a runtime boundary. */
|
|
33
|
-
|
|
42
|
+
decode(input: JsonValue): Effect.Effect<ViewPart<this>, Schema.SchemaError>;
|
|
34
43
|
/** Parse an unknown serialized part without requiring an Effect runtime. */
|
|
35
|
-
|
|
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.
|
|
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<
|
|
66
|
+
readonly schemaVersion: Schema.Literal<Version>;
|
|
58
67
|
} & Fields>;
|
|
59
68
|
partSchema: Schema.Struct<{
|
|
60
|
-
readonly type: Schema.Literal<
|
|
61
|
-
readonly name: Schema.Literal<
|
|
69
|
+
readonly type: Schema.Literal<"data">;
|
|
70
|
+
readonly name: Schema.Literal<Name>;
|
|
62
71
|
readonly data: Schema.Struct<{
|
|
63
|
-
readonly schemaVersion: Schema.Literal<
|
|
72
|
+
readonly schemaVersion: Schema.Literal<Version>;
|
|
64
73
|
} & Fields>;
|
|
65
74
|
}>;
|
|
66
|
-
make: (input: Schema.Struct.
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
37
|
-
const decodePart = Schema.
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
17
|
-
}
|
|
18
|
-
if (
|
|
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.
|
|
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.
|
|
46
|
-
if (
|
|
47
|
-
return session.
|
|
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.
|
|
99
|
-
if (
|
|
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.
|
|
110
|
+
content: decoded.value.message.content,
|
|
104
111
|
metadata: {
|
|
105
|
-
custom: decoded.
|
|
112
|
+
custom: decoded.value.session === undefined
|
|
106
113
|
? {}
|
|
107
114
|
: {
|
|
108
|
-
[assistantChatSessionMetadataKey]: decoded.
|
|
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)
|
package/dist/testing/scenario.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 = {
|