@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,153 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { ChatSessionIdSchema, ChatSessionRevisionSchema, } from "./session.js";
3
+ import { defineView } from "./view.js";
4
+ /** Bounded plain text emitted by a structured chat presenter. */
5
+ export const AssistantTextPartSchema = Schema.Struct({
6
+ type: Schema.Literal("text"),
7
+ text: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(20_000)),
8
+ });
9
+ /** Provider-neutral named data emitted by a structured chat presenter. */
10
+ export const AssistantDataPartSchema = Schema.Struct({
11
+ type: Schema.Literal("data"),
12
+ name: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100), Schema.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/)),
13
+ data: Schema.Unknown,
14
+ });
15
+ /** Message parts transported from a structured chat action to a browser. */
16
+ export const AssistantMessagePartSchema = Schema.Union(AssistantTextPartSchema, AssistantDataPartSchema);
17
+ /** Strict assistant message returned by one structured chat action. */
18
+ export const StructuredChatAssistantMessageSchema = Schema.Struct({
19
+ role: Schema.Literal("assistant"),
20
+ content: Schema.NonEmptyArray(AssistantMessagePartSchema).pipe(Schema.maxItems(20)),
21
+ });
22
+ /** Opaque browser-held reference to one server-owned chat session. */
23
+ export const StructuredChatSessionReferenceSchema = Schema.Struct({
24
+ id: ChatSessionIdSchema,
25
+ revision: ChatSessionRevisionSchema,
26
+ });
27
+ /** Browser request carrying no server-owned chat state. */
28
+ export const StructuredChatTurnRequestSchema = Schema.Struct({
29
+ session: Schema.optional(StructuredChatSessionReferenceSchema),
30
+ message: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(50_000)),
31
+ });
32
+ /** Versioned browser response for one persisted structured chat turn. */
33
+ export const StructuredChatTurnResponseSchema = Schema.Struct({
34
+ schemaVersion: Schema.Literal(1),
35
+ session: Schema.optional(StructuredChatSessionReferenceSchema),
36
+ message: StructuredChatAssistantMessageSchema,
37
+ });
38
+ /** Built-in browser contract for one collect-stage question. */
39
+ export const CollectQuestionView = defineView({
40
+ name: "collect_question",
41
+ version: 1,
42
+ schema: Schema.Struct({
43
+ stage: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100)),
44
+ field: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100)),
45
+ text: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500)),
46
+ options: Schema.Array(Schema.Struct({
47
+ label: Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100)),
48
+ })).pipe(Schema.maxItems(20)),
49
+ }),
50
+ });
51
+ /** Safe reason that application-owned message presentation was rejected. */
52
+ export class InvalidChatPresentation extends Schema.TaggedError()("InvalidChatPresentation", { reason: Schema.Literal("invalid_message") }) {
53
+ }
54
+ /** Construct and validate one plain assistant text part. */
55
+ const makeText = (text) => Schema.validateSync(AssistantTextPartSchema)({ type: "text", text });
56
+ /** Constructors for deterministic assistant message parts. */
57
+ export const Text = {
58
+ make: makeText,
59
+ };
60
+ const invalidPresentation = () => new InvalidChatPresentation({ reason: "invalid_message" });
61
+ const parseResponse = (input) => Schema.decodeUnknown(StructuredChatTurnResponseSchema)(input, {
62
+ onExcessProperty: "error",
63
+ }).pipe(Effect.mapError(invalidPresentation));
64
+ const buildPresentation = (evaluate) => Effect.try({
65
+ try: evaluate,
66
+ catch: invalidPresentation,
67
+ });
68
+ /**
69
+ * Present a safe non-progressing notice after a rejected or unavailable turn.
70
+ *
71
+ * Supplying the prior session reference lets the browser retry from the same
72
+ * server-owned state. An initial rejected turn may omit it entirely.
73
+ */
74
+ export const presentChatNotice = (input) => buildPresentation(() => ({
75
+ schemaVersion: 1,
76
+ session: input.session,
77
+ message: {
78
+ role: "assistant",
79
+ content: [makeText(input.text)],
80
+ },
81
+ })).pipe(Effect.flatMap(parseResponse));
82
+ /**
83
+ * Present an application-authored retry question after answer validation.
84
+ *
85
+ * The rejection is non-progressing: pass the browser's prior session
86
+ * reference, if any, so its next answer retries from the same revision.
87
+ */
88
+ export const presentAnswerValidationRejection = (input) => buildPresentation(() => ({
89
+ schemaVersion: 1,
90
+ session: input.session,
91
+ message: {
92
+ role: "assistant",
93
+ content: [
94
+ CollectQuestionView.make({
95
+ stage: input.rejection.stage,
96
+ field: input.rejection.question.field,
97
+ text: input.rejection.question.text,
98
+ options: [
99
+ ...input.rejection.question.options.map(({ label }) => ({
100
+ label,
101
+ })),
102
+ ...(input.rejection.question.escape === undefined
103
+ ? []
104
+ : [input.rejection.question.escape]),
105
+ ],
106
+ }),
107
+ ],
108
+ },
109
+ })).pipe(Effect.flatMap(parseResponse));
110
+ /**
111
+ * Project one persisted chat reply into the strict browser protocol.
112
+ *
113
+ * Questions use the built-in CollectQuestionView unless overridden. Tool
114
+ * results use their validated views unless the application adds text or a
115
+ * different ordered composition.
116
+ */
117
+ export const presentChatReply = (reply, options = {}) => {
118
+ const buildContent = () => {
119
+ if (reply.turn._tag === "Question") {
120
+ // SAFETY: The discriminant narrows the generic Turn to its question
121
+ // member even though TypeScript cannot retain that fact through Extract.
122
+ const questionTurn = reply.turn;
123
+ return options.question?.(questionTurn) ?? [
124
+ CollectQuestionView.make({
125
+ stage: reply.turn.stage,
126
+ field: reply.turn.question.field,
127
+ text: reply.turn.question.text,
128
+ options: [
129
+ ...reply.turn.question.options.map(({ label }) => ({
130
+ label,
131
+ })),
132
+ ...(reply.turn.question.escape === undefined
133
+ ? []
134
+ : [reply.turn.question.escape]),
135
+ ],
136
+ }),
137
+ ];
138
+ }
139
+ // SAFETY: The non-question branch contains only ToolResult and Complete.
140
+ const toolTurn = reply.turn;
141
+ return options.result?.(toolTurn) ?? reply.turn.result.views;
142
+ };
143
+ return buildPresentation(buildContent).pipe(Effect.flatMap((content) => parseResponse({
144
+ schemaVersion: 1,
145
+ session: {
146
+ id: reply.sessionId,
147
+ revision: reply.revision,
148
+ },
149
+ message: { role: "assistant", content },
150
+ })), Effect.withSpan("popcomputer.structured_chat.presentation.reply", {
151
+ attributes: { stage: reply.turn.stage },
152
+ }));
153
+ };
@@ -0,0 +1,52 @@
1
+ /** One application-authored question whose wording never changes. */
2
+ export interface FixedQuestion {
3
+ readonly _tag: "FixedQuestion";
4
+ readonly text: string;
5
+ }
6
+ /** A goal from which a model may phrase one contextual question. */
7
+ export interface AdaptiveQuestion {
8
+ readonly _tag: "AdaptiveQuestion";
9
+ /** Model-facing phrasing goal; never shown to the user. */
10
+ readonly goal: string;
11
+ /** User-facing question shown when no valid model wording is available. */
12
+ readonly fallback: string;
13
+ }
14
+ /** A contextual question with bounded model suggestions and safe fallbacks. */
15
+ export interface AdaptiveChoiceQuestion {
16
+ readonly _tag: "AdaptiveChoiceQuestion";
17
+ readonly prompt: string;
18
+ readonly minimumOptions: number;
19
+ readonly maximumOptions: number;
20
+ readonly fallbackOptions: ReadonlyArray<string>;
21
+ }
22
+ /** One typed answer offered by an application-authored choice question. */
23
+ export interface QuestionChoice<Value> {
24
+ readonly label: string;
25
+ readonly value: Value;
26
+ }
27
+ /** One application-authored question with a closed set of typed answers. */
28
+ export interface ChoiceQuestion<Value> {
29
+ readonly _tag: "ChoiceQuestion";
30
+ readonly text: string;
31
+ readonly options: readonly [
32
+ QuestionChoice<Value>,
33
+ ...ReadonlyArray<QuestionChoice<Value>>
34
+ ];
35
+ }
36
+ /** Question strategies supported by a collect stage. */
37
+ export type QuestionDefinition<Value = unknown> = FixedQuestion | AdaptiveQuestion | (Value extends string ? AdaptiveChoiceQuestion : never) | ChoiceQuestion<Value>;
38
+ /** Minimum runtime union retained for any typed question definition. */
39
+ export type QuestionDefinitionContract = FixedQuestion | AdaptiveQuestion | AdaptiveChoiceQuestion | ChoiceQuestion<unknown>;
40
+ /** Constructors for static, adaptive, and typed choice questions. */
41
+ export declare const Question: {
42
+ readonly fixed: (text: string) => FixedQuestion;
43
+ readonly adaptive: (goal: string, options: {
44
+ readonly fallback: string;
45
+ }) => AdaptiveQuestion;
46
+ readonly adaptiveChoice: (prompt: string, options: {
47
+ readonly minimumOptions: number;
48
+ readonly maximumOptions: number;
49
+ readonly fallbackOptions?: ReadonlyArray<string>;
50
+ }) => AdaptiveChoiceQuestion;
51
+ readonly choice: <const Options extends readonly [QuestionChoice<unknown>, ...ReadonlyArray<QuestionChoice<unknown>>]>(text: string, options: Options) => ChoiceQuestion<Options[number]["value"]>;
52
+ };
@@ -0,0 +1,62 @@
1
+ import { Schema, unsafeCoerce } from "effect";
2
+ const QuestionTextSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500));
3
+ const QuestionGoalSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(1_000));
4
+ const ChoiceLabelSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100));
5
+ const fixed = (text) => ({
6
+ _tag: "FixedQuestion",
7
+ text: Schema.decodeSync(QuestionTextSchema)(text),
8
+ });
9
+ const adaptive = (goal, options) => ({
10
+ _tag: "AdaptiveQuestion",
11
+ goal: Schema.decodeSync(QuestionGoalSchema)(goal),
12
+ fallback: Schema.decodeSync(QuestionTextSchema)(options.fallback),
13
+ });
14
+ const adaptiveChoice = (prompt, options) => {
15
+ const minimumOptions = Schema.decodeSync(Schema.Number.pipe(Schema.int(), Schema.between(1, 20)))(options.minimumOptions);
16
+ const maximumOptions = Schema.decodeSync(Schema.Number.pipe(Schema.int(), Schema.between(1, 20)))(options.maximumOptions);
17
+ if (minimumOptions > maximumOptions) {
18
+ throw new Error("Adaptive choice minimumOptions cannot exceed maximumOptions");
19
+ }
20
+ const fallbackOptions = (options.fallbackOptions ?? []).map((label) => Schema.decodeSync(ChoiceLabelSchema)(label));
21
+ const normalizedFallbacks = fallbackOptions.map((label) => label.toLocaleLowerCase("en"));
22
+ if (new Set(normalizedFallbacks).size !== fallbackOptions.length) {
23
+ throw new Error("Adaptive choice fallback options must be unique");
24
+ }
25
+ if (fallbackOptions.length > 0 &&
26
+ (fallbackOptions.length < minimumOptions ||
27
+ fallbackOptions.length > maximumOptions)) {
28
+ throw new Error("Adaptive choice fallback options must satisfy the configured bounds");
29
+ }
30
+ return {
31
+ _tag: "AdaptiveChoiceQuestion",
32
+ prompt: Schema.decodeSync(QuestionTextSchema)(prompt),
33
+ minimumOptions,
34
+ maximumOptions,
35
+ fallbackOptions,
36
+ };
37
+ };
38
+ const choice = (text, options) => {
39
+ const labels = options.map(({ label }) => Schema.decodeSync(ChoiceLabelSchema)(label));
40
+ const normalized = labels.map((label) => label.toLocaleLowerCase("en"));
41
+ if (new Set(normalized).size !== normalized.length) {
42
+ throw new Error("Choice question labels must be unique");
43
+ }
44
+ const parsedOptions = options.map((option, index) => ({
45
+ ...option,
46
+ label: labels[index] ?? option.label,
47
+ }));
48
+ // SAFETY: map preserves the non-empty tuple length and each option's value;
49
+ // every replacement label was parsed at the same array index.
50
+ return {
51
+ _tag: "ChoiceQuestion",
52
+ text: Schema.decodeSync(QuestionTextSchema)(text),
53
+ options: unsafeCoerce(parsedOptions),
54
+ };
55
+ };
56
+ /** Constructors for static, adaptive, and typed choice questions. */
57
+ export const Question = {
58
+ fixed,
59
+ adaptive,
60
+ adaptiveChoice,
61
+ choice,
62
+ };
@@ -0,0 +1,14 @@
1
+ import { type StructuredDefinition } from "./definition.js";
2
+ /** Package-owned opt-in policy for bounded standard conversation repair. */
3
+ export interface StandardRepair extends StructuredDefinition<"repair"> {
4
+ readonly _tag: "StandardRepair";
5
+ readonly maximumCorrections: number;
6
+ }
7
+ /** Options for standard correction detection and state repair. */
8
+ export interface StandardRepairOptions {
9
+ readonly maximumCorrections?: number;
10
+ }
11
+ /** Opt-in conversation-repair policies. */
12
+ export declare const Repair: {
13
+ readonly standard: (options?: StandardRepairOptions) => StandardRepair;
14
+ };
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+ import { structuredDefinition, } from "./definition.js";
3
+ const maximumCorrectionsSchema = Schema.Number.pipe(Schema.int(), Schema.between(1, 20));
4
+ const standard = (options = {}) => structuredDefinition("repair")({
5
+ _tag: "StandardRepair",
6
+ maximumCorrections: Schema.decodeSync(maximumCorrectionsSchema)(options.maximumCorrections ?? 5),
7
+ });
8
+ /** Opt-in conversation-repair policies. */
9
+ export const Repair = { standard };
@@ -0,0 +1,78 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import { type UntrustedMessage } from "./model.js";
3
+ /** Stable application-owned identifier for one chat session. */
4
+ export declare const ChatSessionIdSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
5
+ /** Optional application-owned partition for otherwise public session IDs. */
6
+ export declare const ChatSessionNamespaceSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
7
+ /** Opaque optimistic revision emitted by a session store adapter. */
8
+ export declare const ChatSessionRevisionSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
9
+ /** Persisted session snapshot revalidated by the chat runtime after loading. */
10
+ export declare const ChatSessionSnapshotSchema: Schema.Struct<{
11
+ revision: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
12
+ state: typeof Schema.Unknown;
13
+ messages: Schema.filter<Schema.Array$<Schema.Struct<{
14
+ role: Schema.Literal<["user", "assistant"]>;
15
+ content: Schema.filter<typeof Schema.NonEmptyTrimmedString>;
16
+ }>>>;
17
+ }>;
18
+ /** Parsed persisted chat session snapshot. */
19
+ export type ChatSessionSnapshot = Schema.Schema.Type<typeof ChatSessionSnapshotSchema>;
20
+ /** Successful optimistic session replacement returned by an adapter. */
21
+ export declare const ChatSessionReplacementSchema: Schema.Struct<{
22
+ revision: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
23
+ }>;
24
+ /** Successful optimistic session replacement returned by an adapter. */
25
+ export type ChatSessionReplacement = Schema.Schema.Type<typeof ChatSessionReplacementSchema>;
26
+ /** Safe reason that a session store dependency was unavailable. */
27
+ export declare const ChatSessionStoreUnavailableReasonSchema: Schema.Literal<["load_failed", "write_failed"]>;
28
+ declare const ChatSessionStoreUnavailable_base: Schema.TaggedErrorClass<ChatSessionStoreUnavailable, "ChatSessionStoreUnavailable", {
29
+ readonly _tag: Schema.tag<"ChatSessionStoreUnavailable">;
30
+ } & {
31
+ reason: Schema.Literal<["load_failed", "write_failed"]>;
32
+ }>;
33
+ /** A session store adapter could not load or replace state. */
34
+ export declare class ChatSessionStoreUnavailable extends ChatSessionStoreUnavailable_base {
35
+ }
36
+ declare const ChatSessionConflict_base: Schema.TaggedErrorClass<ChatSessionConflict, "ChatSessionConflict", {
37
+ readonly _tag: Schema.tag<"ChatSessionConflict">;
38
+ } & {
39
+ reason: Schema.Literal<["concurrent_update"]>;
40
+ }>;
41
+ /** A concurrent or stale transition attempted to replace newer state. */
42
+ export declare class ChatSessionConflict extends ChatSessionConflict_base {
43
+ }
44
+ /** Safe reason that session input or persisted data was rejected. */
45
+ export declare const InvalidChatSessionReasonSchema: Schema.Literal<["invalid_input", "invalid_snapshot", "invalid_state", "invalid_replacement", "history_limit"]>;
46
+ declare const InvalidChatSession_base: Schema.TaggedErrorClass<InvalidChatSession, "InvalidChatSession", {
47
+ readonly _tag: Schema.tag<"InvalidChatSession">;
48
+ } & {
49
+ reason: Schema.Literal<["invalid_input", "invalid_snapshot", "invalid_state", "invalid_replacement", "history_limit"]>;
50
+ }>;
51
+ /** Session input or persisted state failed runtime validation. */
52
+ export declare class InvalidChatSession extends InvalidChatSession_base {
53
+ }
54
+ /** Stable scope supplied to every session store operation. */
55
+ export interface ChatSessionScope {
56
+ readonly namespace: string;
57
+ readonly sessionId: string;
58
+ readonly chat: string;
59
+ readonly version: number;
60
+ }
61
+ /** Complete optimistic replacement supplied to a session store adapter. */
62
+ export interface ReplaceChatSessionInput extends ChatSessionScope {
63
+ readonly expectedRevision: string | null;
64
+ readonly state: unknown;
65
+ readonly messages: ReadonlyArray<UntrustedMessage>;
66
+ }
67
+ /** Narrow persistence seam for server-owned structured chat sessions. */
68
+ export interface ChatSessionStoreService {
69
+ /** Load one raw snapshot, or null when the session has not started. */
70
+ readonly load: (scope: ChatSessionScope) => Effect.Effect<unknown | null, ChatSessionStoreUnavailable>;
71
+ /** Atomically replace one complete session at its expected revision. */
72
+ readonly replace: (input: ReplaceChatSessionInput) => Effect.Effect<unknown, ChatSessionStoreUnavailable | ChatSessionConflict>;
73
+ }
74
+ declare const ChatSessionStore_base: Context.TagClass<ChatSessionStore, "@popcomputer/structured-chat/ChatSessionStore", ChatSessionStoreService>;
75
+ /** Effect service for the configured server-owned chat session store. */
76
+ export declare class ChatSessionStore extends ChatSessionStore_base {
77
+ }
78
+ export {};
@@ -0,0 +1,34 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import { UntrustedMessageSchema } from "./model.js";
3
+ /** Stable application-owned identifier for one chat session. */
4
+ export const ChatSessionIdSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(200), Schema.pattern(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/));
5
+ /** Optional application-owned partition for otherwise public session IDs. */
6
+ export const ChatSessionNamespaceSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(200), Schema.pattern(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/));
7
+ /** Opaque optimistic revision emitted by a session store adapter. */
8
+ export const ChatSessionRevisionSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(200), Schema.pattern(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/));
9
+ /** Persisted session snapshot revalidated by the chat runtime after loading. */
10
+ export const ChatSessionSnapshotSchema = Schema.Struct({
11
+ revision: ChatSessionRevisionSchema,
12
+ state: Schema.Unknown,
13
+ messages: Schema.Array(UntrustedMessageSchema).pipe(Schema.maxItems(200)),
14
+ });
15
+ /** Successful optimistic session replacement returned by an adapter. */
16
+ export const ChatSessionReplacementSchema = Schema.Struct({
17
+ revision: ChatSessionRevisionSchema,
18
+ });
19
+ /** Safe reason that a session store dependency was unavailable. */
20
+ export const ChatSessionStoreUnavailableReasonSchema = Schema.Literal("load_failed", "write_failed");
21
+ /** A session store adapter could not load or replace state. */
22
+ export class ChatSessionStoreUnavailable extends Schema.TaggedError()("ChatSessionStoreUnavailable", { reason: ChatSessionStoreUnavailableReasonSchema }) {
23
+ }
24
+ /** A concurrent or stale transition attempted to replace newer state. */
25
+ export class ChatSessionConflict extends Schema.TaggedError()("ChatSessionConflict", { reason: Schema.Literal("concurrent_update") }) {
26
+ }
27
+ /** Safe reason that session input or persisted data was rejected. */
28
+ export const InvalidChatSessionReasonSchema = Schema.Literal("invalid_input", "invalid_snapshot", "invalid_state", "invalid_replacement", "history_limit");
29
+ /** Session input or persisted state failed runtime validation. */
30
+ export class InvalidChatSession extends Schema.TaggedError()("InvalidChatSession", { reason: InvalidChatSessionReasonSchema }) {
31
+ }
32
+ /** Effect service for the configured server-owned chat session store. */
33
+ export class ChatSessionStore extends Context.Tag("@popcomputer/structured-chat/ChatSessionStore")() {
34
+ }
@@ -0,0 +1,3 @@
1
+ import { Schema } from "effect";
2
+ /** Stable machine-facing name for one structured chat stage. */
3
+ export declare const StageNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
@@ -0,0 +1,3 @@
1
+ import { Schema } from "effect";
2
+ /** Stable machine-facing name for one structured chat stage. */
3
+ export const StageNameSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100), Schema.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
@@ -0,0 +1,88 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { StructuredChatModel, type ChatModelUnavailable, type UnsupportedModelToolSchema, type UntrustedMessage } from "./model.js";
3
+ import type { ModelGuardError, ModelGuardRequirements, ModelGuardTuple } from "./model-guard.js";
4
+ import { type ToolSet, type ToolSetCall, type ToolSetError, type ToolSetExecution, type ToolSetRequirements, type ToolTuple } from "./tool-set.js";
5
+ import type { CommandDefinitionContract, CommandExecutionContext, InvalidToolCall, InvalidToolProjection, StructuredCommand, ToolCall, ToolExecution, QueryToolDefinitionContract } from "./tool.js";
6
+ import { type StructuredDefinition } from "./definition.js";
7
+ export { StageNameSchema } from "./stage-name.js";
8
+ /** State transition applied after one tool-stage execution. */
9
+ export declare const ToolStageAfterExecutionSchema: Schema.Literal<["stay", "complete"]>;
10
+ /** State transition applied after one tool-stage execution. */
11
+ export type ToolStageAfterExecution = Schema.Schema.Type<typeof ToolStageAfterExecutionSchema>;
12
+ /** Definition input for one stage-scoped, repeatable tool step. */
13
+ export interface DefineToolStageInput<Name extends string, Tools extends ToolTuple, Guards extends ModelGuardTuple> {
14
+ readonly name: Name;
15
+ readonly instructions: readonly [string, ...ReadonlyArray<string>];
16
+ readonly tools: Tools;
17
+ readonly guards?: Guards;
18
+ readonly afterExecution?: ToolStageAfterExecution;
19
+ }
20
+ /** @internal Erased tool-stage behavior consumed by the chat runtime. */
21
+ export interface ToolStageRuntime {
22
+ readonly afterExecution: ToolStageAfterExecution;
23
+ readonly toolNames: ReadonlyArray<string>;
24
+ readonly planWith: (messages: ReadonlyArray<UntrustedMessage>, additionalTool: QueryToolDefinitionContract) => Effect.Effect<ToolCall<string, Schema.Schema.AnyNoContext>, unknown, unknown>;
25
+ readonly execute: (call: ToolCall<string, Schema.Schema.AnyNoContext>) => Effect.Effect<unknown, unknown, unknown>;
26
+ readonly run: (messages: ReadonlyArray<UntrustedMessage>) => Effect.Effect<unknown, unknown, unknown>;
27
+ }
28
+ declare const toolStageRuntime: unique symbol;
29
+ /** Minimum sealed tool-stage shape accepted by a chat definition. */
30
+ export interface ToolStageDefinitionContract extends StructuredDefinition<"tool_stage"> {
31
+ readonly _tag: "ToolStage";
32
+ readonly name: string;
33
+ readonly [toolStageRuntime]: ToolStageRuntime;
34
+ }
35
+ /** @internal Read the erased runtime from an authentic tool stage. */
36
+ export declare const readToolStageRuntime: (stage: ToolStageDefinitionContract) => ToolStageRuntime;
37
+ /** @internal Erased command-stage behavior consumed by the chat runtime. */
38
+ export interface CommandStageRuntime {
39
+ readonly commandName: string;
40
+ readonly run: (messages: ReadonlyArray<UntrustedMessage>, context: CommandExecutionContext) => Effect.Effect<unknown, unknown, unknown>;
41
+ }
42
+ declare const commandStageRuntime: unique symbol;
43
+ /** Minimum sealed terminal command-stage shape accepted by a chat. */
44
+ export interface CommandStageDefinitionContract extends StructuredDefinition<"command_stage"> {
45
+ readonly _tag: "CommandStage";
46
+ readonly name: string;
47
+ readonly command: CommandDefinitionContract;
48
+ readonly [commandStageRuntime]: CommandStageRuntime;
49
+ }
50
+ /** @internal Read the erased runtime from an authentic command stage. */
51
+ export declare const readCommandStageRuntime: (stage: CommandStageDefinitionContract) => CommandStageRuntime;
52
+ /** One stage exposing a closed set of structured tools for each user turn. */
53
+ export interface ToolStage<Name extends string, Tools extends ToolTuple, Guards extends ModelGuardTuple> extends ToolStageDefinitionContract {
54
+ readonly _tag: "ToolStage";
55
+ readonly name: Name;
56
+ readonly toolSet: ToolSet<Tools>;
57
+ readonly guards: Guards;
58
+ readonly afterExecution: ToolStageAfterExecution;
59
+ /** Ask for and parse one stage-scoped call without executing it. */
60
+ readonly plan: (messages: ReadonlyArray<UntrustedMessage>) => Effect.Effect<ToolSetCall<Tools>, ChatModelUnavailable | UnsupportedModelToolSchema | InvalidToolCall | ModelGuardError<Guards>, StructuredChatModel | ModelGuardRequirements<Guards>>;
61
+ /** Run one required tool call against this stage's capabilities. */
62
+ readonly run: (messages: ReadonlyArray<UntrustedMessage>) => Effect.Effect<ToolSetExecution<Tools>, ChatModelUnavailable | UnsupportedModelToolSchema | ToolSetError<Tools> | ModelGuardError<Guards>, StructuredChatModel | ToolSetRequirements<Tools> | ModelGuardRequirements<Guards>>;
63
+ }
64
+ type CommandCall<Command> = Command extends StructuredCommand<infer Name, infer InputSchema, infer _ServerResult, infer _Error, infer _Requirements, infer _ModelSchema, infer _Presenters> ? ToolCall<Name, InputSchema> : never;
65
+ type CommandResult<Command> = Command extends StructuredCommand<infer _Name, infer _InputSchema, infer ServerResult, infer _Error, infer _Requirements, infer ModelSchema, infer Presenters> ? ToolExecution<ServerResult, ModelSchema, Presenters> : never;
66
+ type CommandError<Command> = Command extends StructuredCommand<infer _Name, infer _InputSchema, infer _ServerResult, infer Error, infer _Requirements, infer _ModelSchema, infer _Presenters> ? Error : never;
67
+ type CommandRequirements<Command> = Command extends StructuredCommand<infer _Name, infer _InputSchema, infer _ServerResult, infer _Error, infer Requirements, infer _ModelSchema, infer _Presenters> ? Requirements : never;
68
+ /** Definition input for one exactly-once-intent terminal command stage. */
69
+ export interface DefineCommandStageInput<Name extends string, Command extends CommandDefinitionContract, Guards extends ModelGuardTuple> {
70
+ readonly name: Name;
71
+ readonly instructions: readonly [string, ...ReadonlyArray<string>];
72
+ readonly command: Command;
73
+ readonly guards?: Guards;
74
+ }
75
+ /** One terminal stage exposing exactly one side-effecting command. */
76
+ export interface CommandStage<Name extends string, Command extends CommandDefinitionContract, Guards extends ModelGuardTuple> extends CommandStageDefinitionContract {
77
+ readonly name: Name;
78
+ readonly command: Command;
79
+ readonly guards: Guards;
80
+ readonly plan: (messages: ReadonlyArray<UntrustedMessage>) => Effect.Effect<CommandCall<Command>, ChatModelUnavailable | UnsupportedModelToolSchema | InvalidToolCall | ModelGuardError<Guards>, StructuredChatModel | ModelGuardRequirements<Guards>>;
81
+ readonly run: (messages: ReadonlyArray<UntrustedMessage>, context: CommandExecutionContext) => Effect.Effect<CommandResult<Command>, ChatModelUnavailable | UnsupportedModelToolSchema | InvalidToolCall | InvalidToolProjection | CommandError<Command> | ModelGuardError<Guards>, StructuredChatModel | CommandRequirements<Command> | ModelGuardRequirements<Guards>>;
82
+ }
83
+ /** Constructors for sequential structured chat stages. */
84
+ export declare const Stage: {
85
+ readonly collect: <const Name extends string, const Fields extends import("./collect-stage.js").AnswerFields, const Guards extends ModelGuardTuple = readonly []>(definition: import("./collect-stage.js").DefineCollectStageInput<Name, Fields, Guards>) => import("./collect-stage.js").CollectStage<Name, Fields, Guards>;
86
+ readonly tools: <const Name extends string, const Tools extends ToolTuple, const Guards extends ModelGuardTuple = readonly []>(definition: DefineToolStageInput<Name, Tools, Guards>) => ToolStage<Name, Tools, Guards>;
87
+ readonly command: <const Name extends string, const Command extends CommandDefinitionContract, const Guards extends ModelGuardTuple = readonly []>(definition: DefineCommandStageInput<Name, Command, Guards>) => CommandStage<Name, Command, Guards>;
88
+ };
@@ -0,0 +1,104 @@
1
+ import { Effect, Schema, unsafeCoerce } from "effect";
2
+ import { Instruction, planToolCall, StructuredChatModel, } from "./model.js";
3
+ import { defineToolSet, } from "./tool-set.js";
4
+ import { defineCollectStage } from "./collect-stage.js";
5
+ import { StageNameSchema } from "./stage-name.js";
6
+ import { structuredDefinition, } from "./definition.js";
7
+ export { StageNameSchema } from "./stage-name.js";
8
+ /** State transition applied after one tool-stage execution. */
9
+ export const ToolStageAfterExecutionSchema = Schema.Literal("stay", "complete");
10
+ const toolStageRuntime = Symbol("@popcomputer/structured-chat/ToolStageRuntime");
11
+ /** @internal Read the erased runtime from an authentic tool stage. */
12
+ export const readToolStageRuntime = (stage) => stage[toolStageRuntime];
13
+ const commandStageRuntime = Symbol("@popcomputer/structured-chat/CommandStageRuntime");
14
+ /** @internal Read the erased runtime from an authentic command stage. */
15
+ export const readCommandStageRuntime = (stage) => stage[commandStageRuntime];
16
+ const defineToolStage = (definition) => {
17
+ Schema.decodeSync(StageNameSchema)(definition.name);
18
+ const instructions = definition.instructions.map(Instruction.make);
19
+ const toolSet = defineToolSet(...definition.tools);
20
+ // SAFETY: when guards are omitted, Guards uses its readonly [] default; an
21
+ // explicitly supplied tuple is returned unchanged.
22
+ const guards = definition.guards ?? unsafeCoerce([]);
23
+ const afterExecution = Schema.decodeSync(ToolStageAfterExecutionSchema)(definition.afterExecution ?? "stay");
24
+ const plan = (messages) => planToolCall({
25
+ instructions,
26
+ messages,
27
+ tools: toolSet,
28
+ guards,
29
+ }).pipe(Effect.withSpan("popcomputer.structured_chat.stage.plan", {
30
+ attributes: { stage: definition.name },
31
+ }));
32
+ const run = (messages) => plan(messages).pipe(Effect.flatMap(toolSet.execute));
33
+ // SAFETY: chat repair supplies only a call parsed by a combined set that
34
+ // contains this exact query tuple; non-repair names therefore belong here.
35
+ const executeRuntime = toolSet.execute;
36
+ const planWith = (messages, additionalTool) => {
37
+ // SAFETY: the additional package-owned query and this non-empty exact
38
+ // query tuple form another valid closed tool set for planning only.
39
+ const combined = defineToolSet(additionalTool, ...definition.tools);
40
+ return planToolCall({
41
+ instructions,
42
+ messages,
43
+ tools: combined,
44
+ guards,
45
+ });
46
+ };
47
+ return structuredDefinition("tool_stage")({
48
+ _tag: "ToolStage",
49
+ name: definition.name,
50
+ toolSet,
51
+ guards,
52
+ afterExecution,
53
+ plan,
54
+ run,
55
+ [toolStageRuntime]: {
56
+ afterExecution,
57
+ toolNames: definition.tools.map(({ name }) => name),
58
+ planWith,
59
+ execute: executeRuntime,
60
+ run,
61
+ },
62
+ });
63
+ };
64
+ const defineCommandStage = (definition) => {
65
+ Schema.decodeSync(StageNameSchema)(definition.name);
66
+ const instructions = definition.instructions.map(Instruction.make);
67
+ // SAFETY: when omitted, Guards is its readonly [] default.
68
+ const guards = definition.guards ?? unsafeCoerce([]);
69
+ const planner = {
70
+ models: [definition.command.model],
71
+ // SAFETY: this command parses its literal name and exact input schema.
72
+ parseCall: definition.command.parseCall,
73
+ };
74
+ const plan = (messages) => planToolCall({ instructions, messages, tools: planner, guards }).pipe(Effect.withSpan("popcomputer.structured_chat.command_stage.plan", {
75
+ attributes: { stage: definition.name },
76
+ }));
77
+ // SAFETY: Command has the package-owned identity and command operation;
78
+ // its parsed call and runtime execute input originate from one definition.
79
+ const runtimeCommand = unsafeCoerce(definition.command);
80
+ const runRuntime = (messages, context) => plan(messages).pipe(Effect.flatMap((call) => runtimeCommand.execute(call.arguments, context)), Effect.withSpan("popcomputer.structured_chat.command_stage.run", {
81
+ attributes: { stage: definition.name },
82
+ }));
83
+ // SAFETY: failures and requirements are not recovered; the command's
84
+ // projections and result are preserved by its own execute operation.
85
+ const run = runRuntime;
86
+ return structuredDefinition("command_stage")({
87
+ _tag: "CommandStage",
88
+ name: definition.name,
89
+ command: definition.command,
90
+ guards,
91
+ plan,
92
+ run,
93
+ [commandStageRuntime]: {
94
+ commandName: definition.command.name,
95
+ run: runRuntime,
96
+ },
97
+ });
98
+ };
99
+ /** Constructors for sequential structured chat stages. */
100
+ export const Stage = {
101
+ collect: defineCollectStage,
102
+ tools: defineToolStage,
103
+ command: defineCommandStage,
104
+ };