@popcomputer/structured-chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +780 -0
  3. package/dist/adapters/openai-compatible-model.d.ts +89 -0
  4. package/dist/adapters/openai-compatible-model.js +231 -0
  5. package/dist/core/answer.d.ts +73 -0
  6. package/dist/core/answer.js +60 -0
  7. package/dist/core/chat.d.ts +124 -0
  8. package/dist/core/chat.js +490 -0
  9. package/dist/core/collect-stage.d.ts +203 -0
  10. package/dist/core/collect-stage.js +645 -0
  11. package/dist/core/command.d.ts +16 -0
  12. package/dist/core/command.js +17 -0
  13. package/dist/core/definition.d.ts +10 -0
  14. package/dist/core/definition.js +14 -0
  15. package/dist/core/json-value.d.ts +11 -0
  16. package/dist/core/json-value.js +3 -0
  17. package/dist/core/model-guard.d.ts +52 -0
  18. package/dist/core/model-guard.js +37 -0
  19. package/dist/core/model.d.ts +99 -0
  20. package/dist/core/model.js +109 -0
  21. package/dist/core/protocol.d.ts +257 -0
  22. package/dist/core/protocol.js +153 -0
  23. package/dist/core/question.d.ts +52 -0
  24. package/dist/core/question.js +62 -0
  25. package/dist/core/repair.d.ts +14 -0
  26. package/dist/core/repair.js +9 -0
  27. package/dist/core/session.d.ts +78 -0
  28. package/dist/core/session.js +34 -0
  29. package/dist/core/stage-name.d.ts +3 -0
  30. package/dist/core/stage-name.js +3 -0
  31. package/dist/core/stage.d.ts +88 -0
  32. package/dist/core/stage.js +104 -0
  33. package/dist/core/tool-set.d.ts +49 -0
  34. package/dist/core/tool-set.js +66 -0
  35. package/dist/core/tool.d.ts +149 -0
  36. package/dist/core/tool.js +215 -0
  37. package/dist/core/view.d.ts +95 -0
  38. package/dist/core/view.js +69 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.js +16 -0
  41. package/dist/integrations/assistant-ui.d.ts +95 -0
  42. package/dist/integrations/assistant-ui.js +114 -0
  43. package/dist/testing/in-memory-session-store.d.ts +4 -0
  44. package/dist/testing/in-memory-session-store.js +38 -0
  45. package/dist/testing/scenario.d.ts +59 -0
  46. package/dist/testing/scenario.js +147 -0
  47. package/dist/testing.d.ts +4 -0
  48. package/dist/testing.js +2 -0
  49. package/examples/agency-search.ts +101 -0
  50. package/examples/answer-modes.ts +92 -0
  51. package/examples/prompt-injection-policy.ts +66 -0
  52. package/package.json +89 -0
@@ -0,0 +1,16 @@
1
+ export { AssistantDataPartSchema, AssistantMessagePartSchema, AssistantTextPartSchema, CollectQuestionView, InvalidChatPresentation, presentAnswerValidationRejection, presentChatNotice, presentChatReply, StructuredChatAssistantMessageSchema, StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, Text, type AssistantMessagePart, type PresentChatReplyOptions, type StructuredChatAssistantMessage, type StructuredChatSessionReference, type StructuredChatTurnRequest, type StructuredChatTurnResponse, } from "./core/protocol.js";
2
+ export { makeStructuredChatModel, ModelProvider, structuredChatModelLayer, StructuredChatModelIdSchema, StructuredChatProviderIdSchema, StructuredChatRequestTimeoutSchema, type CloudflareWorkersAIProviderConfig, type OpenAIProviderConfig, type StructuredChatModelConfig, type StructuredChatModelId, type StructuredChatProvider, type StructuredChatProviderId, type StructuredChatProviderRequest, } from "./adapters/openai-compatible-model.js";
3
+ export { ChatNameSchema, ChatVersionSchema, defineChat, InvalidChatTransition, InvalidChatTransitionReasonSchema, type ChatDefinition, type ChatError, type ChatRequirements, type ChatReply, type ChatReplyError, type ChatReplyInput, type ChatStageStates, type ChatStageTuple, type ChatState, type ChatTurn, type DefineChatInput, } from "./core/chat.js";
4
+ export { Answer, AnswerModeSchema, type AnswerDefinition, type AnswerDefinitionContract, type AnswerMode, type DefineAnswerInput, type DefineUnvalidatedAnswerInput, type DefineValidatedAnswerInput, } from "./core/answer.js";
5
+ export { InvalidCollectStageResponse, InvalidCollectStageResponseReasonSchema, type AcceptedAnswer, type AcceptedAnswerEvidence, AnswerValidationRejected, type AnswerFields, type CollectAcceptedAnswers, type CollectAnswerValidationError, type CollectAnswerValidationRequirements, type CollectAnswers, type CollectQuestionPolicy, type CollectStage, type CollectStagePrompt, type CollectStageQuestion, type CollectStageState, type CollectStageTurn, type DefineCollectStageInput, type IssuedCollectQuestion, } from "./core/collect-stage.js";
6
+ export { defineTool, defineCommand, InvalidToolCall, InvalidToolCallReasonSchema, InvalidToolProjection, InvalidToolProjectionReasonSchema, Tool, ToolDescriptionSchema, ToolNameSchema, type DefineToolInput, type DefineCommandInput, type CommandDefinitionContract, type CommandExecutionContext, type ModelToolDefinition, type StructuredTool, type StructuredCommand, type ToolBoundaryParseError, type ToolCall, type ToolExecution, type ToolPresenter, type ToolViewPart, } from "./core/tool.js";
7
+ export { defineToolSet, type ToolSet, type ToolSetCall, type ToolSetError, type ToolSetExecution, type ToolSetRequirements, type ToolTuple, } from "./core/tool-set.js";
8
+ export { ChatModelUnavailable, ChatModelUnavailableReasonSchema, ConversationRoleSchema, Instruction, Message, runToolStep, StructuredChatModel, UnsupportedModelToolSchema, UnsupportedModelToolSchemaReasonSchema, TrustedInstructionSchema, UntrustedMessageSchema, type RunToolStepInput, type StructuredChatModelService, type ToolModelRequest, type TrustedInstruction, type UntrustedMessage, } from "./core/model.js";
9
+ export { defineModelGuard, ModelGuardNameSchema, type DefineModelGuardInput, type ModelGuard, type ModelGuardCall, type ModelGuardCallContext, type ModelGuardContext, type ModelGuardError, type ModelGuardRequirements, type ModelGuardTuple, } from "./core/model-guard.js";
10
+ export { Stage, StageNameSchema, ToolStageAfterExecutionSchema, type DefineToolStageInput, type DefineCommandStageInput, type CommandStage, type ToolStage, type ToolStageAfterExecution, } from "./core/stage.js";
11
+ export { CommandIdSchema, deriveCommandId, type CommandId, type CommandIdentityInput, } from "./core/command.js";
12
+ export { Repair, type StandardRepair, type StandardRepairOptions, } from "./core/repair.js";
13
+ export { Question, type AdaptiveQuestion, type AdaptiveChoiceQuestion, type ChoiceQuestion, type FixedQuestion, type QuestionChoice, type QuestionDefinition, type QuestionDefinitionContract, } from "./core/question.js";
14
+ export { ChatSessionConflict, ChatSessionIdSchema, ChatSessionNamespaceSchema, ChatSessionReplacementSchema, ChatSessionRevisionSchema, ChatSessionSnapshotSchema, ChatSessionStore, ChatSessionStoreUnavailable, ChatSessionStoreUnavailableReasonSchema, InvalidChatSession, InvalidChatSessionReasonSchema, type ChatSessionReplacement, type ChatSessionScope, type ChatSessionSnapshot, type ChatSessionStoreService, type ReplaceChatSessionInput, } from "./core/session.js";
15
+ export { defineView, ViewNameSchema, ViewVersionSchema, type DefineViewInput, type ViewData, type ViewDefinition, type ViewDefinitionContract, type ViewInput, type ViewPart, } from "./core/view.js";
16
+ export { JsonValueSchema, type JsonObject, type JsonPrimitive, type JsonValue, } from "./core/json-value.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export { AssistantDataPartSchema, AssistantMessagePartSchema, AssistantTextPartSchema, CollectQuestionView, InvalidChatPresentation, presentAnswerValidationRejection, presentChatNotice, presentChatReply, StructuredChatAssistantMessageSchema, StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, Text, } from "./core/protocol.js";
2
+ export { makeStructuredChatModel, ModelProvider, structuredChatModelLayer, StructuredChatModelIdSchema, StructuredChatProviderIdSchema, StructuredChatRequestTimeoutSchema, } from "./adapters/openai-compatible-model.js";
3
+ export { ChatNameSchema, ChatVersionSchema, defineChat, InvalidChatTransition, InvalidChatTransitionReasonSchema, } from "./core/chat.js";
4
+ export { Answer, AnswerModeSchema, } from "./core/answer.js";
5
+ export { InvalidCollectStageResponse, InvalidCollectStageResponseReasonSchema, AnswerValidationRejected, } from "./core/collect-stage.js";
6
+ export { defineTool, defineCommand, InvalidToolCall, InvalidToolCallReasonSchema, InvalidToolProjection, InvalidToolProjectionReasonSchema, Tool, ToolDescriptionSchema, ToolNameSchema, } from "./core/tool.js";
7
+ export { defineToolSet, } from "./core/tool-set.js";
8
+ export { ChatModelUnavailable, ChatModelUnavailableReasonSchema, ConversationRoleSchema, Instruction, Message, runToolStep, StructuredChatModel, UnsupportedModelToolSchema, UnsupportedModelToolSchemaReasonSchema, TrustedInstructionSchema, UntrustedMessageSchema, } from "./core/model.js";
9
+ export { defineModelGuard, ModelGuardNameSchema, } from "./core/model-guard.js";
10
+ export { Stage, StageNameSchema, ToolStageAfterExecutionSchema, } from "./core/stage.js";
11
+ export { CommandIdSchema, deriveCommandId, } from "./core/command.js";
12
+ export { Repair, } from "./core/repair.js";
13
+ export { Question, } from "./core/question.js";
14
+ export { ChatSessionConflict, ChatSessionIdSchema, ChatSessionNamespaceSchema, ChatSessionReplacementSchema, ChatSessionRevisionSchema, ChatSessionSnapshotSchema, ChatSessionStore, ChatSessionStoreUnavailable, ChatSessionStoreUnavailableReasonSchema, InvalidChatSession, InvalidChatSessionReasonSchema, } from "./core/session.js";
15
+ export { defineView, ViewNameSchema, ViewVersionSchema, } from "./core/view.js";
16
+ export { JsonValueSchema, } from "./core/json-value.js";
@@ -0,0 +1,95 @@
1
+ import { type ComponentType, type FC } from "react";
2
+ import type { ViewData, ViewDefinitionContract } from "../core/view.js";
3
+ import { type StructuredChatSessionReference, type StructuredChatAssistantMessage } from "../core/protocol.js";
4
+ /** Runtime status supplied by assistant-ui to a data-part renderer. */
5
+ export type AssistantViewPartStatus = {
6
+ readonly type: "running";
7
+ } | {
8
+ readonly type: "complete";
9
+ } | {
10
+ readonly type: "incomplete";
11
+ readonly reason: "cancelled" | "length" | "content-filter" | "other" | "error";
12
+ readonly error?: unknown;
13
+ } | {
14
+ readonly type: "requires-action";
15
+ readonly reason: "interrupt";
16
+ };
17
+ /** Stable data-part props consumed without exposing assistant-ui internals. */
18
+ export interface AssistantDataMessagePartProps<Data = unknown> {
19
+ readonly type: "data";
20
+ readonly name: string;
21
+ readonly data: Data;
22
+ readonly status: AssistantViewPartStatus;
23
+ }
24
+ /** Parsed props supplied to one schema-bound assistant-ui data renderer. */
25
+ export type AssistantViewRenderProps<View extends ViewDefinitionContract> = Omit<AssistantDataMessagePartProps, "data"> & {
26
+ readonly data: ViewData<View>;
27
+ };
28
+ /** Renderer and safe fallback for one schema-bound assistant-ui view. */
29
+ export interface AssistantViewConfig<View extends ViewDefinitionContract> {
30
+ readonly render: ComponentType<AssistantViewRenderProps<View>>;
31
+ readonly fallback?: ComponentType<AssistantDataMessagePartProps>;
32
+ }
33
+ /** Structural data-UI component returned by assistant-ui registration. */
34
+ export type AssistantDataUI = FC & {
35
+ readonly unstable_data: {
36
+ readonly name: string;
37
+ readonly render: ComponentType<AssistantDataMessagePartProps>;
38
+ };
39
+ };
40
+ /**
41
+ * Register one defineView contract as a strictly decoded assistant-ui data UI.
42
+ *
43
+ * Unknown or version-mismatched browser data never reaches the application
44
+ * renderer. It renders the configured fallback, or nothing when omitted.
45
+ */
46
+ export declare const makeAssistantView: <View extends ViewDefinitionContract>(view: View, config: AssistantViewConfig<View>) => AssistantDataUI;
47
+ /** assistant-ui metadata key carrying the latest opaque session reference. */
48
+ export declare const assistantChatSessionMetadataKey: "popcomputerStructuredChatSession";
49
+ /** Small fetch capability required by the assistant-ui chat adapter. */
50
+ export type AssistantChatFetch = (input: string, init: RequestInit) => Promise<Response>;
51
+ /** Browser dependencies for one structured assistant-ui endpoint. */
52
+ export interface AssistantChatModelAdapterOptions {
53
+ readonly endpoint: string;
54
+ readonly fetch?: AssistantChatFetch;
55
+ }
56
+ /** Minimum assistant message shape consumed by the browser adapter. */
57
+ export interface AssistantChatThreadMessage {
58
+ readonly role: string;
59
+ readonly content: ReadonlyArray<{
60
+ readonly type: "text";
61
+ readonly text: string;
62
+ } | {
63
+ readonly type: string;
64
+ }>;
65
+ readonly attachments?: ReadonlyArray<unknown>;
66
+ readonly metadata: {
67
+ readonly custom: {
68
+ readonly [assistantChatSessionMetadataKey]?: unknown;
69
+ };
70
+ };
71
+ }
72
+ /** Assistant runtime input consumed without importing its nominal types. */
73
+ export interface AssistantChatModelRunInput {
74
+ readonly messages: ReadonlyArray<AssistantChatThreadMessage>;
75
+ readonly abortSignal: AbortSignal;
76
+ }
77
+ /** Structural chat adapter that remains compatible across peer installations. */
78
+ export interface AssistantChatModelAdapter {
79
+ readonly run: (input: AssistantChatModelRunInput) => Promise<{
80
+ readonly content: StructuredChatAssistantMessage["content"];
81
+ readonly metadata: {
82
+ readonly custom: Readonly<Record<never, never>> | {
83
+ readonly [assistantChatSessionMetadataKey]: StructuredChatSessionReference;
84
+ };
85
+ };
86
+ }>;
87
+ }
88
+ /**
89
+ * Adapt assistant-ui to one server-owned structured chat endpoint.
90
+ *
91
+ * Only the latest text and opaque prior revision cross the browser boundary.
92
+ * Tools, instructions, history, answer state, and stage position stay server
93
+ * owned.
94
+ */
95
+ export declare const makeAssistantChatModelAdapter: (options: AssistantChatModelAdapterOptions) => AssistantChatModelAdapter;
@@ -0,0 +1,114 @@
1
+ import { makeAssistantDataUI, } from "@assistant-ui/core/react";
2
+ import { Either, Schema } from "effect";
3
+ import { createElement } from "react";
4
+ import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, } from "../core/protocol.js";
5
+ /**
6
+ * Register one defineView contract as a strictly decoded assistant-ui data UI.
7
+ *
8
+ * Unknown or version-mismatched browser data never reaches the application
9
+ * renderer. It renders the configured fallback, or nothing when omitted.
10
+ */
11
+ export const makeAssistantView = (view, config) => {
12
+ const Renderer = (props) => {
13
+ const decoded = Schema.decodeUnknownEither(view.partSchema)({
14
+ type: "data",
15
+ name: view.name,
16
+ data: props.data,
17
+ }, { onExcessProperty: "error" });
18
+ if (Either.isLeft(decoded)) {
19
+ return config.fallback === undefined
20
+ ? null
21
+ : createElement(config.fallback, props);
22
+ }
23
+ return createElement(config.render, {
24
+ ...props,
25
+ data: decoded.right.data,
26
+ });
27
+ };
28
+ return makeAssistantDataUI({
29
+ name: view.name,
30
+ render: Renderer,
31
+ });
32
+ };
33
+ /** assistant-ui metadata key carrying the latest opaque session reference. */
34
+ export const assistantChatSessionMetadataKey = "popcomputerStructuredChatSession";
35
+ const readMessageText = (message) => message.content
36
+ .flatMap((part) => part.type === "text" && "text" in part ? [part.text] : [])
37
+ .join("\n")
38
+ .trim();
39
+ const readLatestSession = (messages) => {
40
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
41
+ const message = messages[index];
42
+ if (message?.role !== "assistant") {
43
+ continue;
44
+ }
45
+ const session = Schema.decodeUnknownEither(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
46
+ if (Either.isRight(session)) {
47
+ return session.right;
48
+ }
49
+ }
50
+ return undefined;
51
+ };
52
+ const readTurnRequest = (messages) => {
53
+ const message = messages.at(-1);
54
+ if (message?.role !== "user") {
55
+ throw new Error("A user message is required");
56
+ }
57
+ if ((message.attachments?.length ?? 0) > 0) {
58
+ throw new Error("Attachments are not supported");
59
+ }
60
+ return Schema.decodeSync(StructuredChatTurnRequestSchema)({
61
+ session: readLatestSession(messages),
62
+ message: readMessageText(message),
63
+ });
64
+ };
65
+ /**
66
+ * Adapt assistant-ui to one server-owned structured chat endpoint.
67
+ *
68
+ * Only the latest text and opaque prior revision cross the browser boundary.
69
+ * Tools, instructions, history, answer state, and stage position stay server
70
+ * owned.
71
+ */
72
+ export const makeAssistantChatModelAdapter = (options) => {
73
+ const fetch_ = options.fetch ??
74
+ ((input, init) => globalThis.fetch(input, init));
75
+ return {
76
+ run: async ({ messages, abortSignal }) => {
77
+ const request = readTurnRequest(messages);
78
+ const response = await fetch_(options.endpoint, {
79
+ method: "POST",
80
+ credentials: "same-origin",
81
+ headers: {
82
+ Accept: "application/json",
83
+ "Content-Type": "application/json",
84
+ },
85
+ body: JSON.stringify(request),
86
+ signal: abortSignal,
87
+ });
88
+ if (!response.ok) {
89
+ throw new Error("Structured chat is temporarily unavailable");
90
+ }
91
+ let body;
92
+ try {
93
+ body = await response.json();
94
+ }
95
+ catch {
96
+ throw new Error("Structured chat returned an invalid response");
97
+ }
98
+ const decoded = Schema.decodeUnknownEither(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
99
+ if (Either.isLeft(decoded)) {
100
+ throw new Error("Structured chat returned an invalid response");
101
+ }
102
+ return {
103
+ content: decoded.right.message.content,
104
+ metadata: {
105
+ custom: decoded.right.session === undefined
106
+ ? {}
107
+ : {
108
+ [assistantChatSessionMetadataKey]: decoded.right.session,
109
+ },
110
+ },
111
+ };
112
+ },
113
+ };
114
+ };
@@ -0,0 +1,4 @@
1
+ import { Layer } from "effect";
2
+ import { ChatSessionStore } from "../core/session.js";
3
+ /** In-memory optimistic session store intended for tests and examples. */
4
+ export declare const inMemoryChatSessionStore: Layer.Layer<ChatSessionStore>;
@@ -0,0 +1,38 @@
1
+ import { Effect, Layer, Ref } from "effect";
2
+ import { ChatSessionConflict, ChatSessionStore, } from "../core/session.js";
3
+ const scopeKey = (scope) => JSON.stringify([
4
+ scope.chat,
5
+ scope.version,
6
+ scope.namespace,
7
+ scope.sessionId,
8
+ ]);
9
+ const nextRevision = (current) => current === undefined
10
+ ? "1"
11
+ : String(Number.parseInt(current.revision, 10) + 1);
12
+ const replaceSnapshot = (sessions, input) => {
13
+ const key = scopeKey(input);
14
+ const current = sessions.get(key);
15
+ if ((current === undefined && input.expectedRevision !== null) ||
16
+ (current !== undefined &&
17
+ input.expectedRevision !== current.revision)) {
18
+ return [
19
+ new ChatSessionConflict({ reason: "concurrent_update" }),
20
+ sessions,
21
+ ];
22
+ }
23
+ const snapshot = {
24
+ revision: nextRevision(current),
25
+ state: input.state,
26
+ messages: input.messages,
27
+ };
28
+ const next = new Map(sessions);
29
+ next.set(key, snapshot);
30
+ return [snapshot, next];
31
+ };
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) => ({
34
+ load: (scope) => Ref.get(sessions).pipe(Effect.map((current) => current.get(scopeKey(scope)) ?? null)),
35
+ replace: (input) => Ref.modify(sessions, (current) => replaceSnapshot(current, input)).pipe(Effect.flatMap((result) => result instanceof ChatSessionConflict
36
+ ? Effect.fail(result)
37
+ : Effect.succeed({ revision: result.revision }))),
38
+ }))));
@@ -0,0 +1,59 @@
1
+ import { Layer, Schema } from "effect";
2
+ import type { AnswerFields, CollectAnswers, CollectStage } from "../core/collect-stage.js";
3
+ import { StructuredChatModel, type ToolModelRequest } from "../core/model.js";
4
+ import type { ModelGuardTuple } from "../core/model-guard.js";
5
+ import type { StructuredTool, ToolDefinitionContract } from "../core/tool.js";
6
+ import { type JsonValue } from "../core/json-value.js";
7
+ declare const scenarioQuote: unique symbol;
8
+ /** One typed scenario value paired with its exact supporting user quote. */
9
+ export interface ScenarioQuote<Value> {
10
+ readonly value: Value;
11
+ readonly quote: string;
12
+ readonly messageIndex?: number;
13
+ readonly [scenarioQuote]: true;
14
+ }
15
+ interface ScenarioStep {
16
+ readonly respond: (request: ToolModelRequest) => JsonValue;
17
+ }
18
+ type QuotedAnswers<Fields extends AnswerFields> = Partial<{
19
+ readonly [Field in keyof Fields]: ScenarioQuote<CollectAnswers<Fields>[Field]>;
20
+ }>;
21
+ type ScenarioNextQuestion<Fields extends AnswerFields> = {
22
+ readonly field: keyof Fields & string;
23
+ readonly text: string;
24
+ readonly options?: ReadonlyArray<{
25
+ readonly label: string;
26
+ }>;
27
+ };
28
+ type ReplaceableField<Fields extends AnswerFields> = {
29
+ [Field in keyof Fields & string]: Fields[Field]["mode"] extends "confirmed" ? never : Field;
30
+ }[keyof Fields & string];
31
+ type ConfirmedField<Fields extends AnswerFields> = {
32
+ [Field in keyof Fields & string]: Fields[Field]["mode"] extends "confirmed" ? Field : never;
33
+ }[keyof Fields & string];
34
+ interface ScenarioRepair {
35
+ readonly respond: (request: ToolModelRequest) => JsonValue;
36
+ }
37
+ type ToolInput<Tool> = Tool extends StructuredTool<infer _Name, infer InputSchema, infer _ServerResult, infer _Error, infer _Requirements, infer _ModelSchema, infer _Presenters, infer _Operation> ? Schema.Schema.Type<InputSchema> : never;
38
+ /** Typed constructors for concise valid transcript scenarios. */
39
+ export declare const Scenario: {
40
+ readonly quoted: <Value>(value: Value, options: {
41
+ readonly quote: string;
42
+ readonly messageIndex?: number;
43
+ }) => ScenarioQuote<Value>;
44
+ readonly answers: <const Name extends string, const Fields extends AnswerFields, const Guards extends ModelGuardTuple>(stage: CollectStage<Name, Fields, Guards>, proposed: QuotedAnswers<Fields>, options?: {
45
+ readonly nextQuestion?: ScenarioNextQuestion<Fields> | null;
46
+ }) => ScenarioStep;
47
+ readonly call: <Tool extends ToolDefinitionContract>(tool: Tool, input: ToolInput<Tool>) => ScenarioStep;
48
+ readonly replace: <const Name extends string, const Fields extends AnswerFields, const Guards extends ModelGuardTuple, const Field extends ReplaceableField<Fields>>(stage: CollectStage<Name, Fields, Guards>, field: Field, value: CollectAnswers<Fields>[Field], options: {
49
+ readonly quote: string;
50
+ readonly messageIndex?: number;
51
+ }) => ScenarioRepair;
52
+ readonly reconfirm: <const Name extends string, const Fields extends AnswerFields, const Guards extends ModelGuardTuple, const Field extends ConfirmedField<Fields>>(stage: CollectStage<Name, Fields, Guards>, field: Field, options: {
53
+ readonly quote: string;
54
+ readonly messageIndex?: number;
55
+ }) => ScenarioRepair;
56
+ readonly repairs: (first: ScenarioRepair, ...remaining: ReadonlyArray<ScenarioRepair>) => ScenarioStep;
57
+ readonly model: (first: ScenarioStep, ...remaining: ReadonlyArray<ScenarioStep>) => Layer.Layer<StructuredChatModel>;
58
+ };
59
+ export {};
@@ -0,0 +1,147 @@
1
+ import { Effect, Layer, Ref, Schema } from "effect";
2
+ import { StructuredChatModel, } from "../core/model.js";
3
+ import { JsonValueSchema, } from "../core/json-value.js";
4
+ const scenarioQuote = Symbol("@popcomputer/structured-chat/testing/ScenarioQuote");
5
+ const ScenarioQuoteSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
6
+ const evidenceIndex = (request, quoted) => {
7
+ if (quoted.messageIndex !== undefined) {
8
+ const message = request.untrustedMessages[quoted.messageIndex];
9
+ if (message === undefined ||
10
+ message.role !== "user" ||
11
+ !message.content.includes(quoted.quote)) {
12
+ throw new Error(`Scenario quote does not match user message ${quoted.messageIndex}`);
13
+ }
14
+ return quoted.messageIndex;
15
+ }
16
+ const matches = request.untrustedMessages.flatMap((message, index) => message.role === "user" && message.content.includes(quoted.quote)
17
+ ? [index]
18
+ : []);
19
+ if (matches.length !== 1) {
20
+ throw new Error(`Scenario quote must match exactly one user message; matched ${matches.length}`);
21
+ }
22
+ const match = matches[0];
23
+ if (match === undefined) {
24
+ throw new Error("Scenario quote match disappeared");
25
+ }
26
+ return match;
27
+ };
28
+ const quoted = (value, options) => {
29
+ const base = {
30
+ value,
31
+ quote: Schema.decodeSync(ScenarioQuoteSchema)(options.quote),
32
+ [scenarioQuote]: true,
33
+ };
34
+ return options.messageIndex === undefined
35
+ ? base
36
+ : { ...base, messageIndex: options.messageIndex };
37
+ };
38
+ const answers = (stage, proposed, options = {}) => ({
39
+ respond: (request) => {
40
+ const encodedAnswers = {};
41
+ for (const field of Object.keys(stage.fields)) {
42
+ encodedAnswers[field] = null;
43
+ }
44
+ const evidence = [];
45
+ // SAFETY: proposed is a mapped type whose only keys are Fields keys.
46
+ for (const field of Object.keys(proposed)) {
47
+ const value = proposed[field];
48
+ const answer = stage.fields[field];
49
+ if (value === undefined || answer === undefined) {
50
+ continue;
51
+ }
52
+ encodedAnswers[field] = Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
53
+ evidenceIndex(request, value);
54
+ evidence.push({
55
+ field,
56
+ quote: value.quote,
57
+ });
58
+ }
59
+ return {
60
+ name: "submit_answers",
61
+ arguments: {
62
+ answers: encodedAnswers,
63
+ evidence,
64
+ nextQuestion: options.nextQuestion === undefined ||
65
+ options.nextQuestion === null
66
+ ? null
67
+ : {
68
+ field: options.nextQuestion.field,
69
+ text: options.nextQuestion.text,
70
+ options: options.nextQuestion.options?.map(({ label }) => label) ?? [],
71
+ },
72
+ },
73
+ };
74
+ },
75
+ });
76
+ const call = (tool, input) => ({
77
+ respond: () => ({
78
+ name: tool.name,
79
+ arguments: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
80
+ }),
81
+ });
82
+ const replace = (stage, field, value, options) => {
83
+ const support = quoted(value, options);
84
+ const answer = stage.fields[field];
85
+ if (answer === undefined) {
86
+ throw new Error(`Unknown scenario repair field: ${String(field)}`);
87
+ }
88
+ return {
89
+ respond: (request) => {
90
+ evidenceIndex(request, support);
91
+ return {
92
+ _tag: "ReplaceAcceptedAnswer",
93
+ stage: stage.name,
94
+ field,
95
+ value: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
96
+ evidence: {
97
+ quote: support.quote,
98
+ },
99
+ };
100
+ },
101
+ };
102
+ };
103
+ const reconfirm = (stage, field, options) => {
104
+ const support = quoted(undefined, options);
105
+ return {
106
+ respond: (request) => {
107
+ evidenceIndex(request, support);
108
+ return {
109
+ _tag: "ReconfirmAnswer",
110
+ stage: stage.name,
111
+ field,
112
+ evidence: {
113
+ quote: support.quote,
114
+ },
115
+ };
116
+ },
117
+ };
118
+ };
119
+ const repairs = (first, ...remaining) => ({
120
+ respond: (request) => ({
121
+ name: "apply_conversation_repairs",
122
+ arguments: {
123
+ corrections: [first, ...remaining].map((repair) => repair.respond(request)),
124
+ },
125
+ }),
126
+ });
127
+ const model = (first, ...remaining) => Layer.effect(StructuredChatModel, Ref.make(0).pipe(Effect.map((cursor) => {
128
+ const steps = [first, ...remaining];
129
+ return {
130
+ requestTool: (request) => Ref.getAndUpdate(cursor, (index) => index + 1).pipe(Effect.flatMap((index) => {
131
+ const step = steps[index];
132
+ return step === undefined
133
+ ? Effect.die(new Error(`Scenario model exhausted after ${steps.length} requests`))
134
+ : Effect.sync(() => step.respond(request));
135
+ })),
136
+ };
137
+ })));
138
+ /** Typed constructors for concise valid transcript scenarios. */
139
+ export const Scenario = {
140
+ quoted,
141
+ answers,
142
+ call,
143
+ replace,
144
+ reconfirm,
145
+ repairs,
146
+ model,
147
+ };
@@ -0,0 +1,4 @@
1
+ export type { StructuredTool, ToolCall, ToolExecution, } from "./core/tool.js";
2
+ export type { ViewData, ViewDefinition, ViewPart, } from "./core/view.js";
3
+ export { inMemoryChatSessionStore } from "./testing/in-memory-session-store.js";
4
+ export { Scenario, type ScenarioQuote, } from "./testing/scenario.js";
@@ -0,0 +1,2 @@
1
+ export { inMemoryChatSessionStore } from "./testing/in-memory-session-store.js";
2
+ export { Scenario, } from "./testing/scenario.js";
@@ -0,0 +1,101 @@
1
+ import {
2
+ Context,
3
+ Effect,
4
+ Layer,
5
+ Schema,
6
+ } from "effect"
7
+ import {
8
+ defineTool,
9
+ defineView,
10
+ Message,
11
+ Stage,
12
+ Tool,
13
+ } from "@popcomputer/structured-chat"
14
+
15
+ interface AgencyMatch {
16
+ readonly id: string
17
+ readonly name: string
18
+ readonly reason: string
19
+ readonly internalScore: number
20
+ readonly retrievedText: string
21
+ }
22
+
23
+ class AgencyCatalog extends Context.Tag("AgencyCatalog")<
24
+ AgencyCatalog,
25
+ {
26
+ readonly search: (
27
+ query: string,
28
+ ) => Effect.Effect<ReadonlyArray<AgencyMatch>>
29
+ }
30
+ >() {}
31
+
32
+ const AgencyCards = defineView({
33
+ name: "agency_cards",
34
+ version: 1,
35
+ schema: Schema.Struct({
36
+ agencies: Schema.Array(
37
+ Schema.Struct({
38
+ id: Schema.String,
39
+ name: Schema.String,
40
+ reason: Schema.String,
41
+ }),
42
+ ),
43
+ }),
44
+ })
45
+
46
+ const AgencyEvidence = Schema.Struct({
47
+ matches: Schema.Array(
48
+ Schema.Struct({
49
+ id: Schema.String,
50
+ reason: Schema.String,
51
+ }),
52
+ ),
53
+ })
54
+
55
+ export const SearchAgencies = defineTool({
56
+ name: "search_agencies",
57
+ description: "Find agencies relevant to the project.",
58
+ input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
59
+ execute: ({ query }) =>
60
+ AgencyCatalog.pipe(
61
+ Effect.flatMap((catalog) => catalog.search(query)),
62
+ ),
63
+ }).pipe(
64
+ Tool.modelResult(AgencyEvidence, (matches) => ({
65
+ matches: matches.map(({ id, reason }) => ({ id, reason })),
66
+ })),
67
+ Tool.present(AgencyCards, (matches) => ({
68
+ agencies: matches.map(({ id, name, reason }) => ({
69
+ id,
70
+ name,
71
+ reason,
72
+ })),
73
+ })),
74
+ )
75
+
76
+ export const Matching = Stage.tools({
77
+ name: "matching",
78
+ instructions: ["Route the project brief to one agency search."],
79
+ tools: [SearchAgencies],
80
+ })
81
+
82
+ const AgencyCatalogLive = Layer.succeed(AgencyCatalog, {
83
+ search: () =>
84
+ Effect.succeed([
85
+ {
86
+ id: "agency:1",
87
+ name: "Northbank",
88
+ reason: "Relevant public-sector transformation work.",
89
+ internalScore: 0.93,
90
+ retrievedText: "Untrusted source text remains server-side.",
91
+ },
92
+ ]),
93
+ })
94
+
95
+ export const example = SearchAgencies.execute({
96
+ query: "A public-sector digital service",
97
+ }).pipe(Effect.provide(AgencyCatalogLive))
98
+
99
+ export const stageExample = Matching.run([
100
+ Message.user("We need a public-sector digital service."),
101
+ ])