@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/answer.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
3
|
-
export const AnswerModeSchema = Schema.
|
|
4
|
-
|
|
3
|
+
export const AnswerModeSchema = Schema.Literals([
|
|
4
|
+
"semantic",
|
|
5
|
+
"explicit",
|
|
6
|
+
"confirmed",
|
|
7
|
+
]);
|
|
8
|
+
const AnswerDescriptionSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(1_000));
|
|
5
9
|
const defineAnswer = (mode, schema, input) => {
|
|
6
10
|
if (input.ask._tag === "ChoiceQuestion") {
|
|
7
11
|
for (const option of input.ask.options) {
|
|
@@ -27,18 +31,23 @@ const defineAnswer = (mode, schema, input) => {
|
|
|
27
31
|
description: Schema.decodeSync(AnswerDescriptionSchema)(input.description),
|
|
28
32
|
question: input.ask,
|
|
29
33
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (input.escape === undefined) {
|
|
35
|
+
return input.validate === undefined
|
|
36
|
+
? base
|
|
37
|
+
: {
|
|
38
|
+
...base,
|
|
39
|
+
validate: input.validate,
|
|
40
|
+
reject: input.reject,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const escape = {
|
|
44
|
+
value: Schema.decodeSync(Schema.toType(schema))(input.escape.value),
|
|
45
|
+
};
|
|
38
46
|
return input.validate === undefined
|
|
39
|
-
?
|
|
47
|
+
? { ...base, escape }
|
|
40
48
|
: {
|
|
41
|
-
...
|
|
49
|
+
...base,
|
|
50
|
+
escape,
|
|
42
51
|
validate: input.validate,
|
|
43
52
|
reject: input.reject,
|
|
44
53
|
};
|
package/dist/core/chat.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import type * as ParseResult from "effect/ParseResult";
|
|
3
2
|
import type { AcceptedAnswer, CollectAnswers, CollectStage, CollectStageDefinitionContract, CollectStagePrompt, CollectStageState } from "./collect-stage.js";
|
|
4
3
|
import { type UntrustedMessage } from "./model.js";
|
|
5
4
|
import { type CommandStage, type CommandStageDefinitionContract, type ToolStage, type ToolStageDefinitionContract } from "./stage.js";
|
|
@@ -7,17 +6,15 @@ import type { ToolSetExecution } from "./tool-set.js";
|
|
|
7
6
|
import type { StandardRepair } from "./repair.js";
|
|
8
7
|
import { ChatSessionConflict, ChatSessionStore, InvalidChatSession, type ChatSessionStoreUnavailable } from "./session.js";
|
|
9
8
|
/** Stable machine-facing name for one structured chat definition. */
|
|
10
|
-
export declare const ChatNameSchema: Schema.
|
|
9
|
+
export declare const ChatNameSchema: Schema.Trimmed;
|
|
11
10
|
/** Positive persisted-state version for one structured chat definition. */
|
|
12
|
-
export declare const ChatVersionSchema: Schema.
|
|
11
|
+
export declare const ChatVersionSchema: Schema.Number;
|
|
13
12
|
/** Safe reason that a server-owned chat transition was rejected. */
|
|
14
|
-
export declare const InvalidChatTransitionReasonSchema: Schema.
|
|
15
|
-
declare const InvalidChatTransition_base: Schema.
|
|
16
|
-
readonly
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
reason: Schema.Literal<["already_complete", "invalid_state"]>;
|
|
20
|
-
}>;
|
|
13
|
+
export declare const InvalidChatTransitionReasonSchema: Schema.Literals<readonly ["already_complete", "invalid_state"]>;
|
|
14
|
+
declare const InvalidChatTransition_base: Schema.Class<InvalidChatTransition, Schema.TaggedStruct<"InvalidChatTransition", {
|
|
15
|
+
readonly chat: Schema.Trimmed;
|
|
16
|
+
readonly reason: Schema.Literals<readonly ["already_complete", "invalid_state"]>;
|
|
17
|
+
}>, import("effect/Cause").YieldableError>;
|
|
21
18
|
/** A server-owned chat state cannot perform the requested transition. */
|
|
22
19
|
export declare class InvalidChatTransition extends InvalidChatTransition_base {
|
|
23
20
|
}
|
|
@@ -50,12 +47,12 @@ export interface ChatState<Name extends string, Version extends number, Stages e
|
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
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.
|
|
50
|
+
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.Success<ReturnType<Stage["run"]>>, object> : never;
|
|
54
51
|
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
52
|
/** Failure union produced by any stage in one chat. */
|
|
56
|
-
export type ChatError<Stages extends ChatStageTuple> = InvalidChatTransition | Effect.
|
|
53
|
+
export type ChatError<Stages extends ChatStageTuple> = InvalidChatTransition | Effect.Error<StageEffect<Stages[number]>>;
|
|
57
54
|
/** Effect service union required by any stage in one chat. */
|
|
58
|
-
export type ChatRequirements<Stages extends ChatStageTuple> = Effect.
|
|
55
|
+
export type ChatRequirements<Stages extends ChatStageTuple> = Effect.Services<StageEffect<Stages[number]>>;
|
|
59
56
|
/** Question, ongoing tool result, or terminal result emitted by one turn. */
|
|
60
57
|
export type ChatTurn<Name extends string, Version extends number, Stages extends ChatStageTuple> = {
|
|
61
58
|
readonly _tag: "Question";
|
|
@@ -100,12 +97,12 @@ export interface ChatDefinition<Name extends string, Version extends number, Sta
|
|
|
100
97
|
readonly version: Version;
|
|
101
98
|
readonly stages: Stages;
|
|
102
99
|
readonly repair: StandardRepair | undefined;
|
|
103
|
-
readonly stateSchema: Schema.
|
|
100
|
+
readonly stateSchema: Schema.Codec<ChatState<Name, Version, Stages>, unknown>;
|
|
104
101
|
readonly initialState: ChatState<Name, Version, Stages>;
|
|
105
102
|
/** Read one accepted value together with its supporting transcript data. */
|
|
106
103
|
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
104
|
/** Strictly parse persisted server-owned chat state. */
|
|
108
|
-
readonly parseState: (input: Schema.
|
|
105
|
+
readonly parseState: (input: Schema.Codec.Encoded<Schema.Codec<ChatState<Name, Version, Stages>, unknown>>) => Effect.Effect<ChatState<Name, Version, Stages>, Schema.SchemaError>;
|
|
109
106
|
/** Run the active stage and any immediately reachable tool stage. */
|
|
110
107
|
readonly run: (input: {
|
|
111
108
|
readonly state: ChatState<Name, Version, Stages>;
|
package/dist/core/chat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema
|
|
1
|
+
import { cast, Effect, Schema } from "effect";
|
|
2
2
|
import { readCollectStageRuntime } from "./collect-stage.js";
|
|
3
3
|
import { countUntrustedMessageCharacters, UntrustedMessageSchema, } from "./model.js";
|
|
4
4
|
import { readCommandStageRuntime, readToolStageRuntime, } from "./stage.js";
|
|
@@ -7,11 +7,14 @@ import { deriveCommandId } from "./command.js";
|
|
|
7
7
|
import { defineTool } from "./tool.js";
|
|
8
8
|
import { ChatSessionConflict, ChatSessionIdSchema, ChatSessionNamespaceSchema, ChatSessionReplacementSchema, ChatSessionRevisionSchema, ChatSessionSnapshotSchema, ChatSessionStore, InvalidChatSession, } from "./session.js";
|
|
9
9
|
/** Stable machine-facing name for one structured chat definition. */
|
|
10
|
-
export const ChatNameSchema = Schema.
|
|
10
|
+
export const ChatNameSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100), Schema.isPattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
|
|
11
11
|
/** Positive persisted-state version for one structured chat definition. */
|
|
12
|
-
export const ChatVersionSchema = Schema.Number.
|
|
12
|
+
export const ChatVersionSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 2_147_483_647 }));
|
|
13
13
|
/** Safe reason that a server-owned chat transition was rejected. */
|
|
14
|
-
export const InvalidChatTransitionReasonSchema = Schema.
|
|
14
|
+
export const InvalidChatTransitionReasonSchema = Schema.Literals([
|
|
15
|
+
"already_complete",
|
|
16
|
+
"invalid_state",
|
|
17
|
+
]);
|
|
15
18
|
/** A server-owned chat state cannot perform the requested transition. */
|
|
16
19
|
export class InvalidChatTransition extends Schema.TaggedError()("InvalidChatTransition", {
|
|
17
20
|
chat: ChatNameSchema,
|
|
@@ -90,9 +93,9 @@ export const defineChat = (definition) => {
|
|
|
90
93
|
}
|
|
91
94
|
const correctionSchema = remainingSchemas.length === 0
|
|
92
95
|
? firstSchema
|
|
93
|
-
: Schema.Union(firstSchema, ...remainingSchemas);
|
|
96
|
+
: Schema.Union([firstSchema, ...remainingSchemas]);
|
|
94
97
|
const input = Schema.Struct({
|
|
95
|
-
corrections: Schema.NonEmptyArray(correctionSchema).
|
|
98
|
+
corrections: Schema.NonEmptyArray(correctionSchema).check(Schema.isMaxLength(repair.maximumCorrections)),
|
|
96
99
|
});
|
|
97
100
|
return defineTool({
|
|
98
101
|
name: repairToolName,
|
|
@@ -164,8 +167,11 @@ export const defineChat = (definition) => {
|
|
|
164
167
|
const baseStateFields = {
|
|
165
168
|
schemaVersion: Schema.Literal(definition.version),
|
|
166
169
|
chat: Schema.Literal(definition.name),
|
|
167
|
-
stage: Schema.Number.
|
|
168
|
-
|
|
170
|
+
stage: Schema.Number.check(Schema.isInt(), Schema.isBetween({
|
|
171
|
+
minimum: 0,
|
|
172
|
+
maximum: definition.stages.length - 1,
|
|
173
|
+
})),
|
|
174
|
+
status: Schema.Literals(["active", "complete"]),
|
|
169
175
|
stages: Schema.Struct(stateFields),
|
|
170
176
|
};
|
|
171
177
|
const rawStateSchema = repair === undefined
|
|
@@ -173,18 +179,21 @@ export const defineChat = (definition) => {
|
|
|
173
179
|
: Schema.Struct({
|
|
174
180
|
...baseStateFields,
|
|
175
181
|
repair: Schema.Struct({
|
|
176
|
-
pendingStages: Schema.Array(Schema.Number.
|
|
182
|
+
pendingStages: Schema.Array(Schema.Number.check(Schema.isInt(), Schema.isBetween({
|
|
183
|
+
minimum: 0,
|
|
184
|
+
maximum: finalStageIndex - 1,
|
|
185
|
+
}))).check(Schema.isMaxLength(20)),
|
|
177
186
|
}),
|
|
178
187
|
});
|
|
179
188
|
// SAFETY: the conditional repair field is erased only for applying the
|
|
180
189
|
// shared semantic predicate; stateSchema below restores the public type.
|
|
181
|
-
const runtimeStateSchema =
|
|
182
|
-
const refinedStateSchema = runtimeStateSchema.
|
|
190
|
+
const runtimeStateSchema = cast(rawStateSchema);
|
|
191
|
+
const refinedStateSchema = runtimeStateSchema.check(Schema.makeFilter((state) => isValidRuntimeState(cast(state)), {
|
|
183
192
|
description: "semantically valid structured-chat state",
|
|
184
193
|
}));
|
|
185
194
|
// SAFETY: stage state fields are taken directly from the concrete collect
|
|
186
195
|
// stages, and the remaining envelope fields are exact literals or bounds.
|
|
187
|
-
const stateSchema =
|
|
196
|
+
const stateSchema = cast(refinedStateSchema);
|
|
188
197
|
const baseInitialState = {
|
|
189
198
|
schemaVersion: definition.version,
|
|
190
199
|
chat: definition.name,
|
|
@@ -195,7 +204,7 @@ export const defineChat = (definition) => {
|
|
|
195
204
|
const initialStateInput = repair === undefined
|
|
196
205
|
? baseInitialState
|
|
197
206
|
: { ...baseInitialState, repair: { pendingStages: [] } };
|
|
198
|
-
const initialState = Schema.
|
|
207
|
+
const initialState = Schema.decodeUnknownSync(Schema.toType(stateSchema))(initialStateInput);
|
|
199
208
|
const isGroundedInMessages = (state, messages) => definition.stages.every((stage) => {
|
|
200
209
|
if (stage._tag !== "CollectStage") {
|
|
201
210
|
return true;
|
|
@@ -279,7 +288,7 @@ export const defineChat = (definition) => {
|
|
|
279
288
|
}
|
|
280
289
|
// SAFETY: planWith used the generated repair tool schema, and this
|
|
281
290
|
// branch is selected by that tool's unique literal name.
|
|
282
|
-
const proposal =
|
|
291
|
+
const proposal = cast(call.arguments);
|
|
283
292
|
return applyConversationRepairs(state, messages, proposal.corrections).pipe(Effect.flatMap((repairedState) => Effect.suspend(() => runTrustedRuntime(repairedState, messages, commandContext, false))));
|
|
284
293
|
}));
|
|
285
294
|
}
|
|
@@ -348,14 +357,14 @@ export const defineChat = (definition) => {
|
|
|
348
357
|
const run = (input) => {
|
|
349
358
|
// SAFETY: ChatState is generated from the same stage tuple as the sealed
|
|
350
359
|
// runtime state contract; only generic correlations are erased here.
|
|
351
|
-
const runtimeState =
|
|
360
|
+
const runtimeState = cast(input.state);
|
|
352
361
|
const runtime = runCheckedRuntime(runtimeState, input.messages);
|
|
353
362
|
// SAFETY: runtime dispatch follows the exact Stages tuple and each stage
|
|
354
363
|
// retains its own parsing, errors, dependencies, and output constructor.
|
|
355
|
-
return
|
|
364
|
+
return cast(runtime);
|
|
356
365
|
};
|
|
357
366
|
const reply = (input) => Effect.gen(function* () {
|
|
358
|
-
const parsedInput = yield* Schema.
|
|
367
|
+
const parsedInput = yield* Schema.decodeUnknownEffect(ChatReplyBoundaryInputSchema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
|
|
359
368
|
const store = yield* ChatSessionStore;
|
|
360
369
|
const scope = {
|
|
361
370
|
namespace: parsedInput.namespace ?? "",
|
|
@@ -371,7 +380,7 @@ export const defineChat = (definition) => {
|
|
|
371
380
|
}));
|
|
372
381
|
const snapshot = loaded === null
|
|
373
382
|
? null
|
|
374
|
-
: yield* Schema.
|
|
383
|
+
: yield* Schema.decodeUnknownEffect(ChatSessionSnapshotSchema)(loaded, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_snapshot")));
|
|
375
384
|
if ((snapshot === null &&
|
|
376
385
|
parsedInput.expectedRevision !== undefined) ||
|
|
377
386
|
(snapshot !== null &&
|
|
@@ -380,12 +389,12 @@ export const defineChat = (definition) => {
|
|
|
380
389
|
}
|
|
381
390
|
const state = snapshot === null
|
|
382
391
|
? initialState
|
|
383
|
-
: yield* Schema.
|
|
392
|
+
: yield* Schema.decodeUnknownEffect(stateSchema)(snapshot.state, {
|
|
384
393
|
onExcessProperty: "error",
|
|
385
394
|
}).pipe(Effect.mapError(() => invalidSession("invalid_state")));
|
|
386
395
|
const previousMessages = snapshot?.messages ?? [];
|
|
387
396
|
// SAFETY: stateSchema decoded this definition's exact state envelope.
|
|
388
|
-
const runtimeState =
|
|
397
|
+
const runtimeState = cast(state);
|
|
389
398
|
if (!isGroundedInMessages(runtimeState, previousMessages)) {
|
|
390
399
|
return yield* Effect.fail(invalidSession("invalid_state"));
|
|
391
400
|
}
|
|
@@ -393,7 +402,7 @@ export const defineChat = (definition) => {
|
|
|
393
402
|
maximumPersistedMessages) {
|
|
394
403
|
return yield* Effect.fail(invalidSession("history_limit"));
|
|
395
404
|
}
|
|
396
|
-
const userMessage = yield* Schema.
|
|
405
|
+
const userMessage = yield* Schema.decodeUnknownEffect(UntrustedMessageSchema)({ role: "user", content: parsedInput.message }, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
|
|
397
406
|
const messages = [...previousMessages, userMessage];
|
|
398
407
|
const commandContext = finalStage?._tag === "CommandStage"
|
|
399
408
|
? {
|
|
@@ -414,7 +423,7 @@ export const defineChat = (definition) => {
|
|
|
414
423
|
snapshot !== null &&
|
|
415
424
|
state.status === "active" &&
|
|
416
425
|
state.stage === finalStageIndex);
|
|
417
|
-
const turn = yield*
|
|
426
|
+
const turn = yield* cast(trustedTurn);
|
|
418
427
|
const toolModelContext = turn._tag === "Question"
|
|
419
428
|
? undefined
|
|
420
429
|
: readToolExecutionModelContext(turn.result);
|
|
@@ -438,7 +447,7 @@ export const defineChat = (definition) => {
|
|
|
438
447
|
if (persistedMessages.length > maximumPersistedMessages) {
|
|
439
448
|
return yield* Effect.fail(invalidSession("history_limit"));
|
|
440
449
|
}
|
|
441
|
-
const encodedState = yield* Schema.
|
|
450
|
+
const encodedState = yield* Schema.encodeUnknownEffect(stateSchema)(turn.state, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_state")));
|
|
442
451
|
const replaced = yield* store
|
|
443
452
|
.replace({
|
|
444
453
|
...scope,
|
|
@@ -456,7 +465,7 @@ export const defineChat = (definition) => {
|
|
|
456
465
|
status: turn.state.status,
|
|
457
466
|
},
|
|
458
467
|
}));
|
|
459
|
-
const replacement = yield* Schema.
|
|
468
|
+
const replacement = yield* Schema.decodeUnknownEffect(ChatSessionReplacementSchema)(replaced, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_replacement")));
|
|
460
469
|
return {
|
|
461
470
|
revision: replacement.revision,
|
|
462
471
|
turn,
|
|
@@ -477,11 +486,11 @@ export const defineChat = (definition) => {
|
|
|
477
486
|
}
|
|
478
487
|
// SAFETY: Stage is restricted to this chat's concrete collect stages,
|
|
479
488
|
// Field is restricted to its field keys, and state uses the same tuple.
|
|
480
|
-
const runtimeState =
|
|
489
|
+
const runtimeState = cast(state);
|
|
481
490
|
const accepted = runtimeState.stages[stage.name]?.accepted[field];
|
|
482
|
-
return
|
|
491
|
+
return cast(accepted);
|
|
483
492
|
},
|
|
484
|
-
parseState: (input) => Schema.
|
|
493
|
+
parseState: (input) => Schema.decodeUnknownEffect(stateSchema)(input, {
|
|
485
494
|
onExcessProperty: "error",
|
|
486
495
|
}),
|
|
487
496
|
run,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import type * as ParseResult from "effect/ParseResult";
|
|
3
2
|
import type { AnswerDefinition, AnswerDefinitionContract, AnswerMode } from "./answer.js";
|
|
4
3
|
import { ChatModelUnavailable, StructuredChatModel, type UnsupportedModelToolSchema, type UntrustedMessage } from "./model.js";
|
|
5
4
|
import type { ModelGuardError, ModelGuardRequirements, ModelGuardTuple } from "./model-guard.js";
|
|
@@ -7,13 +6,11 @@ import { InvalidToolCall, type InvalidToolProjection } from "./tool.js";
|
|
|
7
6
|
import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionChoice } from "./question.js";
|
|
8
7
|
import { type StructuredDefinition } from "./definition.js";
|
|
9
8
|
/** Safe reason that a collect-stage model proposal was rejected. */
|
|
10
|
-
export declare const InvalidCollectStageResponseReasonSchema: Schema.
|
|
11
|
-
declare const InvalidCollectStageResponse_base: Schema.
|
|
12
|
-
readonly
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
reason: Schema.Literal<["invalid_evidence", "invalid_repair"]>;
|
|
16
|
-
}>;
|
|
9
|
+
export declare const InvalidCollectStageResponseReasonSchema: Schema.Literals<readonly ["invalid_evidence", "invalid_repair"]>;
|
|
10
|
+
declare const InvalidCollectStageResponse_base: Schema.Class<InvalidCollectStageResponse, Schema.TaggedStruct<"InvalidCollectStageResponse", {
|
|
11
|
+
readonly stage: Schema.Trimmed;
|
|
12
|
+
readonly reason: Schema.Literals<readonly ["invalid_evidence", "invalid_repair"]>;
|
|
13
|
+
}>, import("effect/Cause").YieldableError>;
|
|
17
14
|
/** A collect-stage proposal was not grounded in a user message. */
|
|
18
15
|
export declare class InvalidCollectStageResponse extends InvalidCollectStageResponse_base {
|
|
19
16
|
}
|
|
@@ -104,7 +101,7 @@ export interface CollectStageTurn<Fields extends AnswerFields> {
|
|
|
104
101
|
readonly complete: boolean;
|
|
105
102
|
readonly question: CollectStagePrompt<Fields> | undefined;
|
|
106
103
|
}
|
|
107
|
-
type RuntimeAnswerValue = Schema.Schema.Type<Schema.
|
|
104
|
+
type RuntimeAnswerValue = Schema.Schema.Type<Schema.Codec<unknown, unknown>>;
|
|
108
105
|
type RuntimeAcceptedAnswer = AcceptedAnswer<RuntimeAnswerValue>;
|
|
109
106
|
interface RuntimeCollectStageState {
|
|
110
107
|
readonly accepted: Readonly<Partial<Record<string, RuntimeAcceptedAnswer>>>;
|
|
@@ -139,12 +136,12 @@ interface RuntimeCollectRepairResult {
|
|
|
139
136
|
/** @internal Erased collect-stage behavior consumed by the chat runtime. */
|
|
140
137
|
export interface CollectStageRuntime {
|
|
141
138
|
readonly initialState: RuntimeCollectStageState;
|
|
142
|
-
readonly stateSchema: Schema.
|
|
139
|
+
readonly stateSchema: Schema.Codec<unknown, unknown>;
|
|
143
140
|
readonly isInitial: (state: RuntimeCollectStageState) => boolean;
|
|
144
141
|
readonly isValid: (state: RuntimeCollectStageState) => boolean;
|
|
145
142
|
readonly isGroundedInMessages: (state: RuntimeCollectStageState, messages: ReadonlyArray<UntrustedMessage>) => boolean;
|
|
146
143
|
readonly isComplete: (state: RuntimeCollectStageState) => boolean;
|
|
147
|
-
readonly repairSchema: Schema.
|
|
144
|
+
readonly repairSchema: Schema.Codec<unknown, unknown>;
|
|
148
145
|
readonly applyRepairs: (state: RuntimeCollectStageState, messages: ReadonlyArray<UntrustedMessage>, repairs: ReadonlyArray<RuntimeCollectRepair>) => Effect.Effect<RuntimeCollectRepairResult, unknown, unknown>;
|
|
149
146
|
readonly run: (input: {
|
|
150
147
|
readonly state: RuntimeCollectStageState;
|
|
@@ -180,12 +177,12 @@ export interface CollectStage<Name extends string, Fields extends AnswerFields,
|
|
|
180
177
|
readonly name: Name;
|
|
181
178
|
readonly fields: Fields;
|
|
182
179
|
readonly questions: CollectQuestionPolicy;
|
|
183
|
-
readonly answersSchema: Schema.
|
|
184
|
-
readonly stateSchema: Schema.
|
|
180
|
+
readonly answersSchema: Schema.Codec<CollectAnswers<Fields>, unknown>;
|
|
181
|
+
readonly stateSchema: Schema.Codec<CollectStageState<Fields>, unknown>;
|
|
185
182
|
readonly initialState: CollectStageState<Fields>;
|
|
186
183
|
readonly guards: Guards;
|
|
187
184
|
/** Strictly parse persisted or client-returned stage state. */
|
|
188
|
-
readonly parseState: (input: Schema.
|
|
185
|
+
readonly parseState: (input: Schema.Codec.Encoded<Schema.Codec<CollectStageState<Fields>, unknown>>) => Effect.Effect<CollectStageState<Fields>, Schema.SchemaError>;
|
|
189
186
|
/** Test whether every schema-defined answer has been populated. */
|
|
190
187
|
readonly isComplete: (state: CollectStageState<Fields>) => boolean;
|
|
191
188
|
/** Select the first missing question in schema declaration order. */
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { Data, Effect,
|
|
1
|
+
import { cast, Data, Effect, Result, Schema, Struct } from "effect";
|
|
2
2
|
import { StageNameSchema } from "./stage-name.js";
|
|
3
3
|
import { ChatModelUnavailable, Instruction, runToolStep, StructuredChatModel, } from "./model.js";
|
|
4
4
|
import { defineTool, InvalidToolCall, } from "./tool.js";
|
|
5
5
|
import { defineToolSet } from "./tool-set.js";
|
|
6
6
|
import { structuredDefinition, } from "./definition.js";
|
|
7
7
|
/** Safe reason that a collect-stage model proposal was rejected. */
|
|
8
|
-
export const InvalidCollectStageResponseReasonSchema = Schema.
|
|
8
|
+
export const InvalidCollectStageResponseReasonSchema = Schema.Literals([
|
|
9
|
+
"invalid_evidence",
|
|
10
|
+
"invalid_repair",
|
|
11
|
+
]);
|
|
9
12
|
/** A collect-stage proposal was not grounded in a user message. */
|
|
10
13
|
export class InvalidCollectStageResponse extends Schema.TaggedError()("InvalidCollectStageResponse", {
|
|
11
14
|
stage: StageNameSchema,
|
|
@@ -22,8 +25,8 @@ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
|
22
25
|
/** Define one deterministic schema-derived fact collection stage. */
|
|
23
26
|
export const defineCollectStage = (definition) => {
|
|
24
27
|
Schema.decodeSync(StageNameSchema)(definition.name);
|
|
25
|
-
const questionGuidanceSchema = Schema.
|
|
26
|
-
const questionEscapeSchema = Schema.
|
|
28
|
+
const questionGuidanceSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
|
|
29
|
+
const questionEscapeSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100));
|
|
27
30
|
const questionPolicyBuilder = {};
|
|
28
31
|
if (definition.questions?.guidance !== undefined) {
|
|
29
32
|
questionPolicyBuilder.guidance = Schema.decodeSync(questionGuidanceSchema)(definition.questions.guidance);
|
|
@@ -34,7 +37,7 @@ export const defineCollectStage = (definition) => {
|
|
|
34
37
|
const questions = questionPolicyBuilder;
|
|
35
38
|
// SAFETY: definition.fields is the exact Fields mapping; Object.keys returns
|
|
36
39
|
// only its enumerable string keys.
|
|
37
|
-
const fieldNames =
|
|
40
|
+
const fieldNames = cast(Object.keys(definition.fields));
|
|
38
41
|
if (fieldNames.length === 0) {
|
|
39
42
|
throw new Error("Collect stages require at least one answer field");
|
|
40
43
|
}
|
|
@@ -53,8 +56,8 @@ export const defineCollectStage = (definition) => {
|
|
|
53
56
|
if (firstField === undefined) {
|
|
54
57
|
throw new Error("Collect stages require at least one answer field");
|
|
55
58
|
}
|
|
56
|
-
const fieldSchema = Schema.
|
|
57
|
-
const messageIndexSchema = Schema.Number.
|
|
59
|
+
const fieldSchema = Schema.Literals([firstField, ...remainingFields]);
|
|
60
|
+
const messageIndexSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 1_000_000 }));
|
|
58
61
|
const getAnswer = (field) => {
|
|
59
62
|
const answer = definition.fields[field];
|
|
60
63
|
if (answer === undefined) {
|
|
@@ -91,10 +94,10 @@ export const defineCollectStage = (definition) => {
|
|
|
91
94
|
}
|
|
92
95
|
const answerSchemaEntries = fieldNames.map((field) => [field, getAnswer(field).schema]);
|
|
93
96
|
// SAFETY: every entry uses one exact Fields key and its corresponding schema.
|
|
94
|
-
const answerSchemas =
|
|
97
|
+
const answerSchemas = cast(Object.fromEntries(answerSchemaEntries));
|
|
95
98
|
const rawAnswersSchema = Schema.Struct(answerSchemas);
|
|
96
|
-
const evidenceQuoteSchema = Schema.
|
|
97
|
-
const questionTextSchema = Schema.
|
|
99
|
+
const evidenceQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
|
|
100
|
+
const questionTextSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(500));
|
|
98
101
|
const acceptedEvidenceSchema = Schema.Struct({
|
|
99
102
|
messageIndex: messageIndexSchema,
|
|
100
103
|
quote: evidenceQuoteSchema,
|
|
@@ -126,10 +129,10 @@ export const defineCollectStage = (definition) => {
|
|
|
126
129
|
}
|
|
127
130
|
const rawRepairSchema = remainingRepairSchemas.length === 0
|
|
128
131
|
? firstRepairSchema
|
|
129
|
-
: Schema.Union(firstRepairSchema, ...remainingRepairSchemas);
|
|
132
|
+
: Schema.Union([firstRepairSchema, ...remainingRepairSchemas]);
|
|
130
133
|
// SAFETY: every dynamically generated member uses only AnyNoContext field
|
|
131
134
|
// schemas and exact stage, field, and transition literals.
|
|
132
|
-
const repairSchema = rawRepairSchema;
|
|
135
|
+
const repairSchema = cast(rawRepairSchema);
|
|
133
136
|
const acceptedFields = Object.fromEntries(fieldNames.map((field) => [
|
|
134
137
|
field,
|
|
135
138
|
Schema.Struct({
|
|
@@ -145,8 +148,8 @@ export const defineCollectStage = (definition) => {
|
|
|
145
148
|
}),
|
|
146
149
|
]));
|
|
147
150
|
const rawStateSchema = Schema.Struct({
|
|
148
|
-
accepted: Schema.
|
|
149
|
-
asked: Schema.
|
|
151
|
+
accepted: Schema.Struct(acceptedFields).mapFields(Struct.map(Schema.optional)),
|
|
152
|
+
asked: Schema.Struct(askedFields).mapFields(Struct.map(Schema.optional)),
|
|
150
153
|
});
|
|
151
154
|
const isValidState = (state) => {
|
|
152
155
|
return fieldNames.every((field) => {
|
|
@@ -156,49 +159,49 @@ export const defineCollectStage = (definition) => {
|
|
|
156
159
|
hasOwn(state.asked, field));
|
|
157
160
|
});
|
|
158
161
|
};
|
|
159
|
-
const refinedStateSchema = rawStateSchema.
|
|
162
|
+
const refinedStateSchema = rawStateSchema.check(Schema.makeFilter(isValidState, {
|
|
160
163
|
description: "semantically valid collect-stage state",
|
|
161
164
|
}));
|
|
162
165
|
// SAFETY: rawAnswersSchema is created from every field's exact schema.
|
|
163
|
-
const answersSchema =
|
|
166
|
+
const answersSchema = cast(rawAnswersSchema);
|
|
164
167
|
// SAFETY: partial preserves the mapped accepted-answer types, while asked is
|
|
165
168
|
// a record whose keys are restricted to the exact field literal union.
|
|
166
|
-
const stateSchema =
|
|
167
|
-
const initialState = Schema.
|
|
169
|
+
const stateSchema = cast(refinedStateSchema);
|
|
170
|
+
const initialState = Schema.decodeSync(Schema.toType(stateSchema))({
|
|
168
171
|
accepted: {},
|
|
169
172
|
asked: {},
|
|
170
173
|
});
|
|
171
174
|
// SAFETY: when guards are omitted, Guards uses its readonly [] default; an
|
|
172
175
|
// explicitly supplied tuple is returned unchanged.
|
|
173
|
-
const guards = definition.guards ??
|
|
176
|
+
const guards = definition.guards ?? cast([]);
|
|
174
177
|
// SAFETY: every entry is built from one registered AnyNoContext answer
|
|
175
178
|
// schema and adds only the model-wire null representation for absence.
|
|
176
179
|
const proposalAnswerSchemaEntries = fieldNames.map((field) => {
|
|
177
180
|
const answer = getAnswer(field);
|
|
178
181
|
return [
|
|
179
182
|
field,
|
|
180
|
-
Schema.NullOr(answer.schema).
|
|
183
|
+
Schema.NullOr(answer.schema).annotate({
|
|
181
184
|
description: `${answer.mode}: ${answer.description}`,
|
|
182
185
|
}),
|
|
183
186
|
];
|
|
184
187
|
});
|
|
185
188
|
// SAFETY: each entry contains one registered field and its no-context schema.
|
|
186
|
-
const proposalAnswerSchemas =
|
|
189
|
+
const proposalAnswerSchemas = cast(Object.fromEntries(proposalAnswerSchemaEntries));
|
|
187
190
|
const rawProposalSchema = Schema.Struct({
|
|
188
191
|
answers: Schema.Struct(proposalAnswerSchemas),
|
|
189
192
|
evidence: Schema.Array(Schema.Struct({
|
|
190
193
|
field: fieldSchema,
|
|
191
194
|
quote: evidenceQuoteSchema,
|
|
192
|
-
})).
|
|
195
|
+
})).check(Schema.isMaxLength(fieldNames.length)),
|
|
193
196
|
nextQuestion: Schema.NullOr(Schema.Struct({
|
|
194
197
|
field: fieldSchema,
|
|
195
198
|
text: questionTextSchema,
|
|
196
|
-
options: Schema.Array(Schema.
|
|
199
|
+
options: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100))).check(Schema.isMaxLength(20)),
|
|
197
200
|
})),
|
|
198
201
|
});
|
|
199
202
|
// SAFETY: every answer field schema is constrained to AnyNoContext; the
|
|
200
203
|
// generic mapped Struct cannot prove that fact after Object.fromEntries.
|
|
201
|
-
const ProposalSchema = rawProposalSchema;
|
|
204
|
+
const ProposalSchema = cast(rawProposalSchema);
|
|
202
205
|
const submitAnswers = defineTool({
|
|
203
206
|
name: "submit_answers",
|
|
204
207
|
description: "Submit grounded answers from the conversation and optionally phrase the next adaptive question.",
|
|
@@ -314,11 +317,11 @@ export const defineCollectStage = (definition) => {
|
|
|
314
317
|
// A selected label is later submitted as this answer's wire value,
|
|
315
318
|
// so model-authored labels that cannot decode would dead-end the
|
|
316
319
|
// user; fall back to the application-authored options instead.
|
|
317
|
-
const decodeLabel = Schema.
|
|
320
|
+
const decodeLabel = Schema.decodeUnknownResult(getAnswer(pending.field).schema);
|
|
318
321
|
const validOptions = supplied.length < question.minimumOptions ||
|
|
319
322
|
supplied.length > question.maximumOptions ||
|
|
320
323
|
new Set(normalized).size !== normalized.length ||
|
|
321
|
-
supplied.some(({ label }) =>
|
|
324
|
+
supplied.some(({ label }) => Result.isFailure(decodeLabel(label)))
|
|
322
325
|
? undefined
|
|
323
326
|
: supplied;
|
|
324
327
|
const selectedOptions = validOptions ??
|
|
@@ -339,8 +342,8 @@ export const defineCollectStage = (definition) => {
|
|
|
339
342
|
options,
|
|
340
343
|
};
|
|
341
344
|
return questions.escape === undefined
|
|
342
|
-
?
|
|
343
|
-
:
|
|
345
|
+
? cast(prompt)
|
|
346
|
+
: cast({ ...prompt, escape: { label: questions.escape } });
|
|
344
347
|
};
|
|
345
348
|
const askPendingQuestion = (state, messages, adaptive) => {
|
|
346
349
|
const pending = nextQuestion(state);
|
|
@@ -385,8 +388,8 @@ export const defineCollectStage = (definition) => {
|
|
|
385
388
|
options: question._tag === "ChoiceQuestion" ? question.options : [],
|
|
386
389
|
};
|
|
387
390
|
return questions.escape === undefined
|
|
388
|
-
?
|
|
389
|
-
:
|
|
391
|
+
? cast(prompt)
|
|
392
|
+
: cast({ ...prompt, escape: { label: questions.escape } });
|
|
390
393
|
};
|
|
391
394
|
const validateAnswer = (field, value) => {
|
|
392
395
|
const answer = getAnswer(field);
|
|
@@ -395,7 +398,7 @@ export const defineCollectStage = (definition) => {
|
|
|
395
398
|
}
|
|
396
399
|
// SAFETY: field selects the same answer definition whose schema parsed
|
|
397
400
|
// value before validation, preserving that field's validator input.
|
|
398
|
-
const validation =
|
|
401
|
+
const validation = cast(answer.validate);
|
|
399
402
|
return validation(value).pipe(Effect.mapError((error) => new AnswerValidationRejected({
|
|
400
403
|
stage: definition.name,
|
|
401
404
|
field,
|
|
@@ -413,7 +416,7 @@ export const defineCollectStage = (definition) => {
|
|
|
413
416
|
for (const repair of repairs) {
|
|
414
417
|
// SAFETY: the field lookup below rejects names outside Fields before
|
|
415
418
|
// any field-indexed operation runs.
|
|
416
|
-
const field =
|
|
419
|
+
const field = cast(repair.field);
|
|
417
420
|
const answer = definition.fields[field];
|
|
418
421
|
if (answer === undefined ||
|
|
419
422
|
seen.has(field) ||
|
|
@@ -547,7 +550,7 @@ export const defineCollectStage = (definition) => {
|
|
|
547
550
|
};
|
|
548
551
|
// SAFETY: accepted keys come only from fieldNames and every value was
|
|
549
552
|
// decoded by that field's schema before insertion.
|
|
550
|
-
const merged =
|
|
553
|
+
const merged = cast(runtimeMerged);
|
|
551
554
|
if (isComplete(merged)) {
|
|
552
555
|
return {
|
|
553
556
|
state: merged,
|
|
@@ -577,7 +580,7 @@ export const defineCollectStage = (definition) => {
|
|
|
577
580
|
// at the first failure. This keeps application Effects and the selected
|
|
578
581
|
// retry question deterministic. Each validator came from the same
|
|
579
582
|
// concrete Fields mapping used by the public conditional unions.
|
|
580
|
-
return execution;
|
|
583
|
+
return cast(execution);
|
|
581
584
|
};
|
|
582
585
|
const run = ({ state, messages, }) => {
|
|
583
586
|
if (!isValidState(state) || !isGroundedInMessages(state, messages)) {
|
|
@@ -594,12 +597,12 @@ export const defineCollectStage = (definition) => {
|
|
|
594
597
|
error.reason === "invalid_response"), (error) => Effect.logWarning("Falling back to the trusted pending question").pipe(Effect.annotateLogs({
|
|
595
598
|
stage: definition.name,
|
|
596
599
|
errorTag: error._tag,
|
|
597
|
-
}), Effect.as(askPendingQuestion(state, messages, null)))));
|
|
600
|
+
}), Effect.as(askPendingQuestion(state, messages, null))), (error) => Effect.fail(error)));
|
|
598
601
|
};
|
|
599
602
|
// SAFETY: The chat runtime calls these erased operations only after the
|
|
600
603
|
// generated state schema has parsed this exact collect-stage state. The
|
|
601
604
|
// public lower-level run method already requires CollectStageState<Fields>.
|
|
602
|
-
const assumeParsedState = (state) =>
|
|
605
|
+
const assumeParsedState = (state) => cast(state);
|
|
603
606
|
return structuredDefinition("collect_stage")({
|
|
604
607
|
_tag: "CollectStage",
|
|
605
608
|
name: definition.name,
|
|
@@ -609,7 +612,7 @@ export const defineCollectStage = (definition) => {
|
|
|
609
612
|
stateSchema,
|
|
610
613
|
initialState,
|
|
611
614
|
guards,
|
|
612
|
-
parseState: (input) => Schema.
|
|
615
|
+
parseState: (input) => Schema.decodeUnknownEffect(stateSchema)(input, {
|
|
613
616
|
onExcessProperty: "error",
|
|
614
617
|
}),
|
|
615
618
|
isComplete,
|
package/dist/core/command.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
/** Opaque deterministic identity supplied to one command execution. */
|
|
3
|
-
export declare const CommandIdSchema: Schema.brand<Schema.
|
|
3
|
+
export declare const CommandIdSchema: Schema.brand<Schema.String, "CommandId">;
|
|
4
4
|
/** Opaque deterministic identity supplied to one command execution. */
|
|
5
5
|
export type CommandId = Schema.Schema.Type<typeof CommandIdSchema>;
|
|
6
6
|
/** Inputs whose exact tuple identity defines one command attempt. */
|
package/dist/core/command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
/** Opaque deterministic identity supplied to one command execution. */
|
|
3
|
-
export const CommandIdSchema = Schema.String.pipe(Schema.
|
|
3
|
+
export const CommandIdSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^cmd_[0-9a-f]{64}$/)), Schema.brand("CommandId"));
|
|
4
4
|
const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5
5
|
/** Derive the stable idempotency key for one persisted command turn. */
|
|
6
6
|
export const deriveCommandId = (input) => Effect.promise(async () => {
|
|
@@ -8,4 +8,4 @@ export interface JsonObject {
|
|
|
8
8
|
/** Recursively typed value accepted at serialized JSON boundaries. */
|
|
9
9
|
export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray<JsonValue>;
|
|
10
10
|
/** Runtime parser for recursively JSON-serializable values. */
|
|
11
|
-
export declare const JsonValueSchema: Schema.
|
|
11
|
+
export declare const JsonValueSchema: Schema.Codec<JsonValue>;
|