@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.
- package/LICENSE +22 -0
- package/README.md +780 -0
- package/dist/adapters/openai-compatible-model.d.ts +89 -0
- package/dist/adapters/openai-compatible-model.js +231 -0
- package/dist/core/answer.d.ts +73 -0
- package/dist/core/answer.js +60 -0
- package/dist/core/chat.d.ts +124 -0
- package/dist/core/chat.js +490 -0
- package/dist/core/collect-stage.d.ts +203 -0
- package/dist/core/collect-stage.js +645 -0
- package/dist/core/command.d.ts +16 -0
- package/dist/core/command.js +17 -0
- package/dist/core/definition.d.ts +10 -0
- package/dist/core/definition.js +14 -0
- package/dist/core/json-value.d.ts +11 -0
- package/dist/core/json-value.js +3 -0
- package/dist/core/model-guard.d.ts +52 -0
- package/dist/core/model-guard.js +37 -0
- package/dist/core/model.d.ts +99 -0
- package/dist/core/model.js +109 -0
- package/dist/core/protocol.d.ts +257 -0
- package/dist/core/protocol.js +153 -0
- package/dist/core/question.d.ts +52 -0
- package/dist/core/question.js +62 -0
- package/dist/core/repair.d.ts +14 -0
- package/dist/core/repair.js +9 -0
- package/dist/core/session.d.ts +78 -0
- package/dist/core/session.js +34 -0
- package/dist/core/stage-name.d.ts +3 -0
- package/dist/core/stage-name.js +3 -0
- package/dist/core/stage.d.ts +88 -0
- package/dist/core/stage.js +104 -0
- package/dist/core/tool-set.d.ts +49 -0
- package/dist/core/tool-set.js +66 -0
- package/dist/core/tool.d.ts +149 -0
- package/dist/core/tool.js +215 -0
- package/dist/core/view.d.ts +95 -0
- package/dist/core/view.js +69 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/integrations/assistant-ui.d.ts +95 -0
- package/dist/integrations/assistant-ui.js +114 -0
- package/dist/testing/in-memory-session-store.d.ts +4 -0
- package/dist/testing/in-memory-session-store.js +38 -0
- package/dist/testing/scenario.d.ts +59 -0
- package/dist/testing/scenario.js +147 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.js +2 -0
- package/examples/agency-search.ts +101 -0
- package/examples/answer-modes.ts +92 -0
- package/examples/prompt-injection-policy.ts +66 -0
- package/package.json +89 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Layer, Schema } from "effect";
|
|
2
|
+
import { StructuredChatModel, type ChatModelUnavailableReasonSchema, type StructuredChatModelService } from "../core/model.js";
|
|
3
|
+
import type { ModelToolDefinition } from "../core/tool.js";
|
|
4
|
+
import { type JsonValue } from "../core/json-value.js";
|
|
5
|
+
/** Bounded timeout for one provider tool-call request. */
|
|
6
|
+
export declare const StructuredChatRequestTimeoutSchema: Schema.filter<Schema.filter<typeof Schema.Number>>;
|
|
7
|
+
interface OpenAICompatibleMessage {
|
|
8
|
+
readonly role: "system" | "user";
|
|
9
|
+
readonly content: string;
|
|
10
|
+
}
|
|
11
|
+
interface OpenAICompatibleTool {
|
|
12
|
+
readonly type: "function";
|
|
13
|
+
readonly function: {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly parameters: ModelToolDefinition["inputSchema"];
|
|
17
|
+
readonly strict?: true;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
type OpenAICompatibleInputValue = JsonValue | ReadonlyArray<OpenAICompatibleMessage> | ReadonlyArray<OpenAICompatibleTool>;
|
|
21
|
+
interface OpenAICompatibleInput {
|
|
22
|
+
readonly [key: string]: OpenAICompatibleInputValue;
|
|
23
|
+
}
|
|
24
|
+
declare const OpenAICompatibleToolArgumentsSchema: Schema.Literal<["guided", "strict"]>;
|
|
25
|
+
type OpenAICompatibleToolArguments = Schema.Schema.Type<typeof OpenAICompatibleToolArgumentsSchema>;
|
|
26
|
+
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
27
|
+
export declare const StructuredChatModelIdSchema: Schema.filter<typeof Schema.NonEmptyTrimmedString>;
|
|
28
|
+
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
29
|
+
export type StructuredChatModelId = Schema.Schema.Type<typeof StructuredChatModelIdSchema>;
|
|
30
|
+
/** One provider request with its parsed model identifier. */
|
|
31
|
+
export interface StructuredChatProviderRequest {
|
|
32
|
+
readonly model: StructuredChatModelId;
|
|
33
|
+
readonly input: OpenAICompatibleInput;
|
|
34
|
+
}
|
|
35
|
+
interface OpenAICompatibleProviderConfig {
|
|
36
|
+
readonly model: string;
|
|
37
|
+
readonly complete: (request: StructuredChatProviderRequest, signal: AbortSignal) => Promise<JsonValue>;
|
|
38
|
+
readonly requestOptions?: Readonly<Record<string, JsonValue>>;
|
|
39
|
+
}
|
|
40
|
+
/** Configuration for the built-in Cloudflare Workers AI provider. */
|
|
41
|
+
export interface CloudflareWorkersAIProviderConfig extends OpenAICompatibleProviderConfig {
|
|
42
|
+
}
|
|
43
|
+
/** Configuration for the built-in OpenAI provider. */
|
|
44
|
+
export interface OpenAIProviderConfig extends OpenAICompatibleProviderConfig {
|
|
45
|
+
}
|
|
46
|
+
/** Stable identifier for a built-in model provider. */
|
|
47
|
+
export declare const StructuredChatProviderIdSchema: Schema.Literal<["cloudflare-workers-ai", "openai"]>;
|
|
48
|
+
/** Stable identifier for a built-in model provider. */
|
|
49
|
+
export type StructuredChatProviderId = Schema.Schema.Type<typeof StructuredChatProviderIdSchema>;
|
|
50
|
+
interface StructuredChatProviderRuntime {
|
|
51
|
+
readonly toolArguments: OpenAICompatibleToolArguments;
|
|
52
|
+
readonly complete: (input: OpenAICompatibleInput, signal: AbortSignal) => Promise<JsonValue>;
|
|
53
|
+
readonly requestOptions: Readonly<Record<string, JsonValue>>;
|
|
54
|
+
}
|
|
55
|
+
declare const StructuredChatProviderRuntime: unique symbol;
|
|
56
|
+
/** Opaque provider definition consumed by the structured model adapter. */
|
|
57
|
+
export interface StructuredChatProvider {
|
|
58
|
+
readonly id: StructuredChatProviderId;
|
|
59
|
+
readonly model: StructuredChatModelId;
|
|
60
|
+
readonly [StructuredChatProviderRuntime]: StructuredChatProviderRuntime;
|
|
61
|
+
}
|
|
62
|
+
/** Configuration for one provider-backed structured chat model. */
|
|
63
|
+
export interface StructuredChatModelConfig {
|
|
64
|
+
readonly provider: StructuredChatProvider;
|
|
65
|
+
readonly timeoutMilliseconds: number;
|
|
66
|
+
readonly classifyError?: (cause: unknown) => Schema.Schema.Type<typeof ChatModelUnavailableReasonSchema>;
|
|
67
|
+
}
|
|
68
|
+
/** Built-in provider definitions that own their tool-call guarantees. */
|
|
69
|
+
export declare const ModelProvider: {
|
|
70
|
+
/**
|
|
71
|
+
* Define a Cloudflare Workers AI model.
|
|
72
|
+
*
|
|
73
|
+
* Workers AI schemas guide generation but are always validated after the
|
|
74
|
+
* response because Cloudflare does not guarantee schema-constrained output.
|
|
75
|
+
*/
|
|
76
|
+
readonly cloudflareWorkersAI: (config: CloudflareWorkersAIProviderConfig) => StructuredChatProvider;
|
|
77
|
+
/**
|
|
78
|
+
* Define an OpenAI model.
|
|
79
|
+
*
|
|
80
|
+
* Known Structured Outputs model families use strict function arguments.
|
|
81
|
+
* Unknown and older model identifiers conservatively use schema guidance.
|
|
82
|
+
*/
|
|
83
|
+
readonly openAI: (config: OpenAIProviderConfig) => StructuredChatProvider;
|
|
84
|
+
};
|
|
85
|
+
/** Build a structured chat model around one provider definition. */
|
|
86
|
+
export declare const makeStructuredChatModel: (config: StructuredChatModelConfig) => StructuredChatModelService;
|
|
87
|
+
/** Build an Effect layer for one provider-backed structured chat model. */
|
|
88
|
+
export declare const structuredChatModelLayer: (config: StructuredChatModelConfig) => Layer.Layer<StructuredChatModel>;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Effect, Either, Layer, Schema } from "effect";
|
|
2
|
+
import { ChatModelUnavailable, StructuredChatModel, UnsupportedModelToolSchema, } from "../core/model.js";
|
|
3
|
+
import { JsonValueSchema, } from "../core/json-value.js";
|
|
4
|
+
/** Bounded timeout for one provider tool-call request. */
|
|
5
|
+
export const StructuredChatRequestTimeoutSchema = Schema.Number.pipe(Schema.int(), Schema.between(1, 60_000));
|
|
6
|
+
const OpenAICompatibleToolArgumentsSchema = Schema.Literal("guided", "strict");
|
|
7
|
+
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
8
|
+
export const StructuredChatModelIdSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(200));
|
|
9
|
+
/** Stable identifier for a built-in model provider. */
|
|
10
|
+
export const StructuredChatProviderIdSchema = Schema.Literal("cloudflare-workers-ai", "openai");
|
|
11
|
+
const StructuredChatProviderRuntime = Symbol("@popcomputer/structured-chat/StructuredChatProviderRuntime");
|
|
12
|
+
const openAIModelSupportsStrictToolArguments = (model) => /^(?:chat-latest$|gpt-4o(?:-|$)|gpt-4\.1(?:-|$)|gpt-5(?:[.-]|$)|o3(?:-|$)|o4(?:-|$))/u.test(model);
|
|
13
|
+
const makeProvider = (id, config, toolArguments) => {
|
|
14
|
+
const model = Schema.decodeSync(StructuredChatModelIdSchema)(config.model);
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
model,
|
|
18
|
+
[StructuredChatProviderRuntime]: {
|
|
19
|
+
toolArguments,
|
|
20
|
+
requestOptions: config.requestOptions ?? {},
|
|
21
|
+
complete: (input, signal) => config.complete({ model, input }, signal),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** Built-in provider definitions that own their tool-call guarantees. */
|
|
26
|
+
export const ModelProvider = {
|
|
27
|
+
/**
|
|
28
|
+
* Define a Cloudflare Workers AI model.
|
|
29
|
+
*
|
|
30
|
+
* Workers AI schemas guide generation but are always validated after the
|
|
31
|
+
* response because Cloudflare does not guarantee schema-constrained output.
|
|
32
|
+
*/
|
|
33
|
+
cloudflareWorkersAI: (config) => makeProvider("cloudflare-workers-ai", config, "guided"),
|
|
34
|
+
/**
|
|
35
|
+
* Define an OpenAI model.
|
|
36
|
+
*
|
|
37
|
+
* Known Structured Outputs model families use strict function arguments.
|
|
38
|
+
* Unknown and older model identifiers conservatively use schema guidance.
|
|
39
|
+
*/
|
|
40
|
+
openAI: (config) => {
|
|
41
|
+
const model = Schema.decodeSync(StructuredChatModelIdSchema)(config.model);
|
|
42
|
+
return makeProvider("openai", { ...config, model }, openAIModelSupportsStrictToolArguments(model)
|
|
43
|
+
? "strict"
|
|
44
|
+
: "guided");
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const ToolCallResponseSchema = Schema.Struct({
|
|
48
|
+
choices: Schema.Tuple(Schema.Struct({
|
|
49
|
+
message: Schema.Struct({
|
|
50
|
+
tool_calls: Schema.Tuple(Schema.Struct({
|
|
51
|
+
function: Schema.Struct({
|
|
52
|
+
name: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100)),
|
|
53
|
+
arguments: Schema.String.pipe(Schema.maxLength(20_000)),
|
|
54
|
+
}),
|
|
55
|
+
})),
|
|
56
|
+
}),
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
const unavailable = (reason) => new ChatModelUnavailable({ reason });
|
|
60
|
+
const parseJson = (input) => Effect.try({
|
|
61
|
+
// SAFETY: JSON.parse without a reviver can only return a JSON value when
|
|
62
|
+
// parsing succeeds; failures are mapped to the typed unavailable reason.
|
|
63
|
+
try: () => JSON.parse(input),
|
|
64
|
+
catch: () => unavailable("invalid_response"),
|
|
65
|
+
});
|
|
66
|
+
const JsonSchemaObjectSchema = Schema.Record({
|
|
67
|
+
key: Schema.String,
|
|
68
|
+
value: JsonValueSchema,
|
|
69
|
+
});
|
|
70
|
+
const isJsonSchemaObject = (value) => Schema.is(JsonSchemaObjectSchema)(value);
|
|
71
|
+
const appendJsonPointer = (path, segment) => `${path}/${segment.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
72
|
+
const findStrictObjectIssue = (schema, path) => {
|
|
73
|
+
const objectSchema = schema.type === "object" || isJsonSchemaObject(schema.properties);
|
|
74
|
+
if (objectSchema) {
|
|
75
|
+
if (schema.additionalProperties !== false) {
|
|
76
|
+
return {
|
|
77
|
+
path,
|
|
78
|
+
reason: "additional_properties_allowed",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const properties = isJsonSchemaObject(schema.properties)
|
|
82
|
+
? schema.properties
|
|
83
|
+
: {};
|
|
84
|
+
const required = Array.isArray(schema.required)
|
|
85
|
+
? new Set(schema.required.filter(Schema.is(Schema.String)))
|
|
86
|
+
: new Set();
|
|
87
|
+
for (const property of Object.keys(properties)) {
|
|
88
|
+
if (!required.has(property)) {
|
|
89
|
+
return {
|
|
90
|
+
path: appendJsonPointer(appendJsonPointer(path, "properties"), property),
|
|
91
|
+
reason: "optional_property",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [property, propertySchema] of Object.entries(properties)) {
|
|
96
|
+
if (!isJsonSchemaObject(propertySchema)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const issue = findStrictObjectIssue(propertySchema, appendJsonPointer(appendJsonPointer(path, "properties"), property));
|
|
100
|
+
if (issue !== undefined) {
|
|
101
|
+
return issue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const definitionKey of ["$defs", "definitions"]) {
|
|
106
|
+
const definitions = schema[definitionKey];
|
|
107
|
+
if (!isJsonSchemaObject(definitions)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
111
|
+
if (!isJsonSchemaObject(definition)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const issue = findStrictObjectIssue(definition, appendJsonPointer(appendJsonPointer(path, definitionKey), name));
|
|
115
|
+
if (issue !== undefined) {
|
|
116
|
+
return issue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const unionKey of ["allOf", "anyOf", "oneOf"]) {
|
|
121
|
+
const members = schema[unionKey];
|
|
122
|
+
if (!Array.isArray(members)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const [index, member] of members.entries()) {
|
|
126
|
+
if (!isJsonSchemaObject(member)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const issue = findStrictObjectIssue(member, appendJsonPointer(appendJsonPointer(path, unionKey), String(index)));
|
|
130
|
+
if (issue !== undefined) {
|
|
131
|
+
return issue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const items = schema.items;
|
|
136
|
+
if (isJsonSchemaObject(items)) {
|
|
137
|
+
return findStrictObjectIssue(items, appendJsonPointer(path, "items"));
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
};
|
|
141
|
+
const findStrictSchemaIssue = (schema) => {
|
|
142
|
+
if (schema.type !== "object") {
|
|
143
|
+
return { path: "#", reason: "root_not_object" };
|
|
144
|
+
}
|
|
145
|
+
return findStrictObjectIssue(schema, "#");
|
|
146
|
+
};
|
|
147
|
+
const strictToolIssue = (tool) => {
|
|
148
|
+
const parsedSchema = Schema.decodeUnknownEither(JsonSchemaObjectSchema)(tool.inputSchema, { onExcessProperty: "error" });
|
|
149
|
+
if (Either.isLeft(parsedSchema)) {
|
|
150
|
+
return new UnsupportedModelToolSchema({
|
|
151
|
+
tool: tool.name,
|
|
152
|
+
path: "#",
|
|
153
|
+
reason: "root_not_object",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const issue = findStrictSchemaIssue(parsedSchema.right);
|
|
157
|
+
return issue === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: new UnsupportedModelToolSchema({
|
|
160
|
+
tool: tool.name,
|
|
161
|
+
path: issue.path,
|
|
162
|
+
reason: issue.reason,
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const toProviderTool = (tool, toolArguments) => ({
|
|
166
|
+
type: "function",
|
|
167
|
+
function: toolArguments === "strict"
|
|
168
|
+
? {
|
|
169
|
+
name: tool.name,
|
|
170
|
+
description: tool.description,
|
|
171
|
+
parameters: tool.inputSchema,
|
|
172
|
+
strict: true,
|
|
173
|
+
}
|
|
174
|
+
: {
|
|
175
|
+
name: tool.name,
|
|
176
|
+
description: tool.description,
|
|
177
|
+
parameters: tool.inputSchema,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const toProviderInput = (request, requestOptions, toolArguments) => {
|
|
181
|
+
if (toolArguments === "strict") {
|
|
182
|
+
const unsupported = request.tools
|
|
183
|
+
.map(strictToolIssue)
|
|
184
|
+
.find((issue) => issue !== undefined);
|
|
185
|
+
if (unsupported !== undefined) {
|
|
186
|
+
return Effect.fail(unsupported);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return Effect.succeed({
|
|
190
|
+
...requestOptions,
|
|
191
|
+
messages: [
|
|
192
|
+
{
|
|
193
|
+
role: "system",
|
|
194
|
+
content: request.instructions.join("\n\n"),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
role: "user",
|
|
198
|
+
content: JSON.stringify({
|
|
199
|
+
untrustedConversation: request.untrustedMessages,
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
tools: request.tools.map((tool) => toProviderTool(tool, toolArguments)),
|
|
204
|
+
tool_choice: "required",
|
|
205
|
+
parallel_tool_calls: false,
|
|
206
|
+
stream: false,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
/** Build a structured chat model around one provider definition. */
|
|
210
|
+
export const makeStructuredChatModel = (config) => {
|
|
211
|
+
const timeoutMilliseconds = Schema.decodeSync(StructuredChatRequestTimeoutSchema)(config.timeoutMilliseconds);
|
|
212
|
+
const runtime = config.provider[StructuredChatProviderRuntime];
|
|
213
|
+
const classifyError = config.classifyError ?? (() => "request_failed");
|
|
214
|
+
return {
|
|
215
|
+
requestTool: (request) => toProviderInput(request, runtime.requestOptions, runtime.toolArguments).pipe(Effect.flatMap((input) => Effect.tryPromise({
|
|
216
|
+
try: (signal) => runtime.complete(input, signal),
|
|
217
|
+
catch: (cause) => unavailable(classifyError(cause)),
|
|
218
|
+
})), Effect.timeoutFail({
|
|
219
|
+
duration: timeoutMilliseconds,
|
|
220
|
+
onTimeout: () => unavailable("timed_out"),
|
|
221
|
+
}), Effect.flatMap((response) => Schema.decodeUnknown(ToolCallResponseSchema)(response).pipe(Effect.mapError(() => unavailable("invalid_response")))), Effect.flatMap((response) => {
|
|
222
|
+
const tool = response.choices[0].message.tool_calls[0].function;
|
|
223
|
+
return parseJson(tool.arguments).pipe(Effect.map((arguments_) => ({
|
|
224
|
+
name: tool.name,
|
|
225
|
+
arguments: arguments_,
|
|
226
|
+
})));
|
|
227
|
+
})),
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/** Build an Effect layer for one provider-backed structured chat model. */
|
|
231
|
+
export const structuredChatModelLayer = (config) => Layer.succeed(StructuredChatModel, makeStructuredChatModel(config));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { ChoiceQuestion, FixedQuestion, QuestionDefinition, QuestionDefinitionContract } from "./question.js";
|
|
3
|
+
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
4
|
+
export declare const AnswerModeSchema: Schema.Literal<["semantic", "explicit", "confirmed"]>;
|
|
5
|
+
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
6
|
+
export type AnswerMode = Schema.Schema.Type<typeof AnswerModeSchema>;
|
|
7
|
+
/** Minimum runtime shape retained for every collect-stage answer. */
|
|
8
|
+
export interface AnswerDefinitionContract {
|
|
9
|
+
readonly _tag: "AnswerDefinition";
|
|
10
|
+
readonly mode: AnswerMode;
|
|
11
|
+
readonly schema: Schema.Schema.AnyNoContext;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly question: QuestionDefinitionContract;
|
|
14
|
+
readonly validate?: (value: never) => Effect.Effect<void, unknown, unknown>;
|
|
15
|
+
readonly reject?: {
|
|
16
|
+
readonly ask: FixedQuestion | ChoiceQuestion<unknown>;
|
|
17
|
+
};
|
|
18
|
+
readonly escape?: {
|
|
19
|
+
readonly value: unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** One typed fact required by a collect stage. */
|
|
23
|
+
export interface AnswerDefinition<Mode extends AnswerMode, ValueSchema extends Schema.Schema.AnyNoContext, Error = never, Requirements = never> extends AnswerDefinitionContract {
|
|
24
|
+
readonly mode: Mode;
|
|
25
|
+
readonly schema: ValueSchema;
|
|
26
|
+
readonly question: QuestionDefinition<Schema.Schema.Type<ValueSchema>>;
|
|
27
|
+
readonly validate?: (value: Schema.Schema.Type<ValueSchema>) => Effect.Effect<void, Error, Requirements>;
|
|
28
|
+
readonly reject?: {
|
|
29
|
+
readonly ask: FixedQuestion | ChoiceQuestion<Schema.Schema.Type<ValueSchema>>;
|
|
30
|
+
};
|
|
31
|
+
readonly escape?: {
|
|
32
|
+
readonly value: Schema.Schema.Type<ValueSchema>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
interface DefineAnswerBase<Value> {
|
|
36
|
+
readonly description: string;
|
|
37
|
+
readonly ask: QuestionDefinition<Value>;
|
|
38
|
+
/**
|
|
39
|
+
* Application-authored value accepted when the user chooses the stage's
|
|
40
|
+
* uncertainty escape for this field. Without it, an escaped field stays
|
|
41
|
+
* unresolved and its question is asked again from another angle.
|
|
42
|
+
*/
|
|
43
|
+
readonly escape?: {
|
|
44
|
+
readonly value: Value;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Configuration for an answer accepted solely by its structural schema. */
|
|
48
|
+
export interface DefineUnvalidatedAnswerInput<Value> extends DefineAnswerBase<Value> {
|
|
49
|
+
readonly validate?: undefined;
|
|
50
|
+
readonly reject?: undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Configuration for Effect-native domain acceptance and deterministic retry. */
|
|
53
|
+
export interface DefineValidatedAnswerInput<Value, Error, Requirements> extends DefineAnswerBase<Value> {
|
|
54
|
+
readonly validate: (value: Value) => Effect.Effect<void, Error, Requirements>;
|
|
55
|
+
readonly reject: {
|
|
56
|
+
readonly ask: FixedQuestion | ChoiceQuestion<Value>;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Configuration shared by all answer grounding modes. */
|
|
60
|
+
export type DefineAnswerInput<Value, Error = never, Requirements = never> = DefineUnvalidatedAnswerInput<Value> | DefineValidatedAnswerInput<Value, Error, Requirements>;
|
|
61
|
+
declare function semantic<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"semantic", ValueSchema, never, never>;
|
|
62
|
+
declare function semantic<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"semantic", ValueSchema, Error, Requirements>;
|
|
63
|
+
declare function explicit<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"explicit", ValueSchema, never, never>;
|
|
64
|
+
declare function explicit<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"explicit", ValueSchema, Error, Requirements>;
|
|
65
|
+
declare function confirmed<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"confirmed", ValueSchema, never, never>;
|
|
66
|
+
declare function confirmed<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"confirmed", ValueSchema, Error, Requirements>;
|
|
67
|
+
/** Constructors for semantic, explicit, and explicitly confirmed facts. */
|
|
68
|
+
export declare const Answer: {
|
|
69
|
+
readonly semantic: typeof semantic;
|
|
70
|
+
readonly explicit: typeof explicit;
|
|
71
|
+
readonly confirmed: typeof confirmed;
|
|
72
|
+
};
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
3
|
+
export const AnswerModeSchema = Schema.Literal("semantic", "explicit", "confirmed");
|
|
4
|
+
const AnswerDescriptionSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(1_000));
|
|
5
|
+
const defineAnswer = (mode, schema, input) => {
|
|
6
|
+
if (input.ask._tag === "ChoiceQuestion") {
|
|
7
|
+
for (const option of input.ask.options) {
|
|
8
|
+
Schema.decodeSync(schema)(option.value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (input.ask._tag === "AdaptiveChoiceQuestion") {
|
|
12
|
+
// A selected fallback label is later submitted as this answer's wire
|
|
13
|
+
// value, so every label must decode against the answer schema.
|
|
14
|
+
for (const label of input.ask.fallbackOptions) {
|
|
15
|
+
Schema.decodeSync(schema)(label);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (input.reject?.ask._tag === "ChoiceQuestion") {
|
|
19
|
+
for (const option of input.reject.ask.options) {
|
|
20
|
+
Schema.decodeSync(schema)(option.value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const base = {
|
|
24
|
+
_tag: "AnswerDefinition",
|
|
25
|
+
mode,
|
|
26
|
+
schema,
|
|
27
|
+
description: Schema.decodeSync(AnswerDescriptionSchema)(input.description),
|
|
28
|
+
question: input.ask,
|
|
29
|
+
};
|
|
30
|
+
const withEscape = input.escape === undefined
|
|
31
|
+
? base
|
|
32
|
+
: {
|
|
33
|
+
...base,
|
|
34
|
+
escape: {
|
|
35
|
+
value: Schema.validateSync(schema)(input.escape.value),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
return input.validate === undefined
|
|
39
|
+
? withEscape
|
|
40
|
+
: {
|
|
41
|
+
...withEscape,
|
|
42
|
+
validate: input.validate,
|
|
43
|
+
reject: input.reject,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
function semantic(schema, input) {
|
|
47
|
+
return defineAnswer("semantic", schema, input);
|
|
48
|
+
}
|
|
49
|
+
function explicit(schema, input) {
|
|
50
|
+
return defineAnswer("explicit", schema, input);
|
|
51
|
+
}
|
|
52
|
+
function confirmed(schema, input) {
|
|
53
|
+
return defineAnswer("confirmed", schema, input);
|
|
54
|
+
}
|
|
55
|
+
/** Constructors for semantic, explicit, and explicitly confirmed facts. */
|
|
56
|
+
export const Answer = {
|
|
57
|
+
semantic,
|
|
58
|
+
explicit,
|
|
59
|
+
confirmed,
|
|
60
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type * as ParseResult from "effect/ParseResult";
|
|
3
|
+
import type { AcceptedAnswer, CollectAnswers, CollectStage, CollectStageDefinitionContract, CollectStagePrompt, CollectStageState } from "./collect-stage.js";
|
|
4
|
+
import { type UntrustedMessage } from "./model.js";
|
|
5
|
+
import { type CommandStage, type CommandStageDefinitionContract, type ToolStage, type ToolStageDefinitionContract } from "./stage.js";
|
|
6
|
+
import type { ToolSetExecution } from "./tool-set.js";
|
|
7
|
+
import type { StandardRepair } from "./repair.js";
|
|
8
|
+
import { ChatSessionConflict, ChatSessionStore, InvalidChatSession, type ChatSessionStoreUnavailable } from "./session.js";
|
|
9
|
+
/** Stable machine-facing name for one structured chat definition. */
|
|
10
|
+
export declare const ChatNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
|
|
11
|
+
/** Positive persisted-state version for one structured chat definition. */
|
|
12
|
+
export declare const ChatVersionSchema: Schema.filter<Schema.filter<typeof Schema.Number>>;
|
|
13
|
+
/** Safe reason that a server-owned chat transition was rejected. */
|
|
14
|
+
export declare const InvalidChatTransitionReasonSchema: Schema.Literal<["already_complete", "invalid_state"]>;
|
|
15
|
+
declare const InvalidChatTransition_base: Schema.TaggedErrorClass<InvalidChatTransition, "InvalidChatTransition", {
|
|
16
|
+
readonly _tag: Schema.tag<"InvalidChatTransition">;
|
|
17
|
+
} & {
|
|
18
|
+
chat: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
|
|
19
|
+
reason: Schema.Literal<["already_complete", "invalid_state"]>;
|
|
20
|
+
}>;
|
|
21
|
+
/** A server-owned chat state cannot perform the requested transition. */
|
|
22
|
+
export declare class InvalidChatTransition extends InvalidChatTransition_base {
|
|
23
|
+
}
|
|
24
|
+
/** Minimum runtime identity retained for every structured chat stage. */
|
|
25
|
+
export type ChatStageDefinitionContract = CollectStageDefinitionContract | ToolStageDefinitionContract | CommandStageDefinitionContract;
|
|
26
|
+
/** Non-empty sequential stage tuple accepted by one chat definition. */
|
|
27
|
+
export type ChatStageTuple = readonly [
|
|
28
|
+
ChatStageDefinitionContract,
|
|
29
|
+
...ReadonlyArray<ChatStageDefinitionContract>
|
|
30
|
+
];
|
|
31
|
+
type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Intersection) => void ? Intersection : never;
|
|
32
|
+
type CollectStateEntry<Stage> = Stage extends CollectStage<infer Name, infer Fields, infer _Guards> ? {
|
|
33
|
+
readonly [Key in Name]: CollectStageState<Fields>;
|
|
34
|
+
} : never;
|
|
35
|
+
type ChatCollectStage<Stages extends ChatStageTuple> = Extract<Stages[number], CollectStageDefinitionContract>;
|
|
36
|
+
type CollectFields<Stage> = Stage extends CollectStage<infer _Name, infer Fields, infer _Guards> ? Fields : never;
|
|
37
|
+
/** Persisted state entries derived from every collect stage. */
|
|
38
|
+
export type ChatStageStates<Stages extends ChatStageTuple> = [
|
|
39
|
+
CollectStateEntry<Stages[number]>
|
|
40
|
+
] extends [never] ? Readonly<Record<never, never>> : UnionToIntersection<CollectStateEntry<Stages[number]>>;
|
|
41
|
+
/** Complete server-owned state for one structured chat session. */
|
|
42
|
+
export interface ChatState<Name extends string, Version extends number, Stages extends ChatStageTuple> {
|
|
43
|
+
readonly schemaVersion: Version;
|
|
44
|
+
readonly chat: Name;
|
|
45
|
+
readonly stage: number;
|
|
46
|
+
readonly status: "active" | "complete";
|
|
47
|
+
readonly stages: ChatStageStates<Stages>;
|
|
48
|
+
readonly repair?: {
|
|
49
|
+
readonly pendingStages: ReadonlyArray<number>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
type ChatQuestion<Stage> = Stage extends CollectStage<infer _Name, infer Fields, infer _Guards> ? CollectStagePrompt<Fields> : never;
|
|
53
|
+
type ChatToolExecution<Stage> = Stage extends ToolStage<infer _Name, infer Tools, infer _Guards> ? ToolSetExecution<Tools> : Stage extends CommandStage<infer _Name, infer _Command, infer _Guards> ? Extract<Effect.Effect.Success<ReturnType<Stage["run"]>>, object> : never;
|
|
54
|
+
type StageEffect<Stage> = Stage extends CollectStage<infer _CollectName, infer _Fields, infer _CollectGuards> ? ReturnType<Stage["run"]> : Stage extends ToolStage<infer _ToolName, infer _Tools, infer _ToolGuards> ? ReturnType<Stage["run"]> : Stage extends CommandStage<infer _CommandName, infer _Command, infer _CommandGuards> ? ReturnType<Stage["run"]> : never;
|
|
55
|
+
/** Failure union produced by any stage in one chat. */
|
|
56
|
+
export type ChatError<Stages extends ChatStageTuple> = InvalidChatTransition | Effect.Effect.Error<StageEffect<Stages[number]>>;
|
|
57
|
+
/** Effect service union required by any stage in one chat. */
|
|
58
|
+
export type ChatRequirements<Stages extends ChatStageTuple> = Effect.Effect.Context<StageEffect<Stages[number]>>;
|
|
59
|
+
/** Question, ongoing tool result, or terminal result emitted by one turn. */
|
|
60
|
+
export type ChatTurn<Name extends string, Version extends number, Stages extends ChatStageTuple> = {
|
|
61
|
+
readonly _tag: "Question";
|
|
62
|
+
readonly stage: string;
|
|
63
|
+
readonly state: ChatState<Name, Version, Stages>;
|
|
64
|
+
readonly question: ChatQuestion<Stages[number]>;
|
|
65
|
+
} | {
|
|
66
|
+
readonly _tag: "ToolResult";
|
|
67
|
+
readonly stage: string;
|
|
68
|
+
readonly state: ChatState<Name, Version, Stages>;
|
|
69
|
+
readonly result: ChatToolExecution<Stages[number]>;
|
|
70
|
+
} | {
|
|
71
|
+
readonly _tag: "Complete";
|
|
72
|
+
readonly stage: string;
|
|
73
|
+
readonly state: ChatState<Name, Version, Stages>;
|
|
74
|
+
readonly result: ChatToolExecution<Stages[number]>;
|
|
75
|
+
};
|
|
76
|
+
/** Input for one persisted server-owned chat reply. */
|
|
77
|
+
export interface ChatReplyInput {
|
|
78
|
+
readonly namespace?: string | undefined;
|
|
79
|
+
readonly sessionId: string;
|
|
80
|
+
readonly expectedRevision?: string | undefined;
|
|
81
|
+
readonly message: string;
|
|
82
|
+
}
|
|
83
|
+
/** Persisted result of one server-owned chat reply. */
|
|
84
|
+
export interface ChatReply<Name extends string, Version extends number, Stages extends ChatStageTuple> {
|
|
85
|
+
readonly revision: string;
|
|
86
|
+
readonly turn: ChatTurn<Name, Version, Stages>;
|
|
87
|
+
}
|
|
88
|
+
/** Failure union produced while loading, running, and replacing a session. */
|
|
89
|
+
export type ChatReplyError<Stages extends ChatStageTuple> = ChatError<Stages> | ChatSessionStoreUnavailable | ChatSessionConflict | InvalidChatSession;
|
|
90
|
+
/** Definition input for one sequential structured chat. */
|
|
91
|
+
export interface DefineChatInput<Name extends string, Version extends number, Stages extends ChatStageTuple> {
|
|
92
|
+
readonly name: Name;
|
|
93
|
+
readonly version: Version;
|
|
94
|
+
readonly stages: Stages;
|
|
95
|
+
readonly repair?: StandardRepair;
|
|
96
|
+
}
|
|
97
|
+
/** One schema-defined sequential chat runtime. */
|
|
98
|
+
export interface ChatDefinition<Name extends string, Version extends number, Stages extends ChatStageTuple> {
|
|
99
|
+
readonly name: Name;
|
|
100
|
+
readonly version: Version;
|
|
101
|
+
readonly stages: Stages;
|
|
102
|
+
readonly repair: StandardRepair | undefined;
|
|
103
|
+
readonly stateSchema: Schema.Schema<ChatState<Name, Version, Stages>, unknown, never>;
|
|
104
|
+
readonly initialState: ChatState<Name, Version, Stages>;
|
|
105
|
+
/** Read one accepted value together with its supporting transcript data. */
|
|
106
|
+
readonly getAcceptedAnswer: <Stage extends ChatCollectStage<Stages>, Field extends keyof CollectFields<Stage> & string>(state: ChatState<Name, Version, Stages>, stage: Stage, field: Field) => AcceptedAnswer<CollectAnswers<CollectFields<Stage>>[Field]> | undefined;
|
|
107
|
+
/** Strictly parse persisted server-owned chat state. */
|
|
108
|
+
readonly parseState: (input: Schema.Schema.Encoded<Schema.Schema<ChatState<Name, Version, Stages>, unknown, never>>) => Effect.Effect<ChatState<Name, Version, Stages>, ParseResult.ParseError>;
|
|
109
|
+
/** Run the active stage and any immediately reachable tool stage. */
|
|
110
|
+
readonly run: (input: {
|
|
111
|
+
readonly state: ChatState<Name, Version, Stages>;
|
|
112
|
+
readonly messages: ReadonlyArray<UntrustedMessage>;
|
|
113
|
+
}) => Effect.Effect<ChatTurn<Name, Version, Stages>, ChatError<Stages>, ChatRequirements<Stages>>;
|
|
114
|
+
/** Load, run, and atomically replace one server-owned chat session. */
|
|
115
|
+
readonly reply: (input: ChatReplyInput) => Effect.Effect<ChatReply<Name, Version, Stages>, ChatReplyError<Stages>, ChatSessionStore | ChatRequirements<Stages>>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Define one sequential chat with collectors and one final executable stage.
|
|
119
|
+
*
|
|
120
|
+
* A final query stage remains active by default so later user messages can
|
|
121
|
+
* refine results. A final command stage is always terminal.
|
|
122
|
+
*/
|
|
123
|
+
export declare const defineChat: <const Name extends string, const Version extends number, const Stages extends ChatStageTuple>(definition: DefineChatInput<Name, Version, Stages>) => ChatDefinition<Name, Version, Stages>;
|
|
124
|
+
export {};
|