@adia-ai/agent 0.8.26

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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## [0.8.26] — 2026-08-05
4
+
5
+ ### Added
6
+ - **L1 hardening per CHAT-HARNESS.md laws 1–3 + 6 (WCH-1, gh#596).** `AgentEvent` gains two additive kinds — `progress` (closed, code-owned `ProgressStage` enum: `PROGRESS_STAGES` + `isProgressStage()` guard; the loop emits `sent`/`retry`/`tool`/`done` at its genuine natural points) and `surface` (one generative-UI wire line, envelope-keyed) — both render-only in `reduce()`. A byte-identity baseline gate (`baseline/prompt-equivalence.baseline.json` + `baseline.test.js`, regenerated via `scripts/build/regen-agent-baseline.mjs`) pins that every optional `AgentConfig` axis, when absent, leaves no trace on the derived `ChatOpts`. `frameClientMessage()` + `shouldRunTurn()` (`src/frame.ts`) map a surface action / function result / validation rejection to a distinct, deterministic natural-language user turn — silent-apply kinds (`dataModelUpdate`, `ack`) gate out before framing and throw if framed directly. `agent.sendClientMessage(session, msg)` wires gate → frame → loop.
7
+ - **Initial release — the composable chat-agent harness (gh#579).** `createAgent()` assembles an agent from four declared parts on top of `@adia-ai/llm`: cache-aware prompt layers (`promptLayer`), a JSON-schema'd tool registry with local-fn and endpoint executors plus the bounded execute-and-feed-back loop (`defineTool`, `maxToolRounds`, `onToolCall` gate), declarative workflow cascades (`defineWorkflow` — `until` accepts / terminal steps / `when` skips), and resources in `attach` (application-controlled prompt layer) or `tool` (model-controlled `read_<name>`) mode. Ships the serializable `Session` + the one shared event reducer (`reduce`), and `scriptClient` for deterministic keyless tests.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # `@adia-ai/agent`
2
+
3
+ Composable chat-agent harness. Assemble an agent from four declared parts —
4
+ **prompt layers**, **tools**, **workflows**, **resources** — on top of
5
+ [`@adia-ai/llm`](../llm/README.md). The package owns the tool-call loop, the
6
+ serializable `Session`, and the one shared event reducer; it renders nothing
7
+ (pair it with `web-modules/chat` or any UI).
8
+
9
+ > **Experimental** until the A2UI pipeline migration validates the contracts
10
+ > (tracked in [gh#579](https://github.com/adiahealth/gen-ui-kit/issues/579)).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @adia-ai/agent
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ import {
22
+ createAgent, promptLayer, defineTool, defineWorkflow,
23
+ defineResource, reduce,
24
+ } from '@adia-ai/agent';
25
+
26
+ const agent = createAgent({
27
+ llm: { model: 'claude-sonnet-4-6', proxyUrl: '/api/chat' },
28
+ prompt: [
29
+ promptLayer('identity', 'You are the AdiaUI copilot.', { cache: true }),
30
+ promptLayer('context', () => new Date().toISOString()), // per-turn, never cached
31
+ ],
32
+ tools: [
33
+ defineTool({
34
+ name: 'search_patients',
35
+ description: 'Find a patient by name or MRN',
36
+ inputSchema: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] },
37
+ execute: async ({ q }) => lookup(q), // or endpoint: { url, method }
38
+ }),
39
+ ],
40
+ workflows: [
41
+ defineWorkflow('generate', [
42
+ { name: 'zettel', run: zettel, until: r => r.strategy === 'composition-match' },
43
+ { name: 'free-form', run: freeForm, until: r => r.ok },
44
+ { name: 'monolithic', run: monolithic }, // terminal — always accepted
45
+ ]),
46
+ ],
47
+ resources: [
48
+ defineResource({ name: 'visit-summary', mode: 'attach', get: fetchSummary }), // app-controlled
49
+ defineResource({ name: 'care-plan', mode: 'tool', get: readPlan }), // model-controlled
50
+ ],
51
+ maxToolRounds: 8,
52
+ });
53
+
54
+ let session = agent.createSession(); // plain JSON-safe object — you own persistence
55
+ for await (const event of agent.send(session, 'weather in Oslo?')) {
56
+ session = reduce(session, event); // the one reducer — the single session writer
57
+ render(event); // text | thinking | tool_use | tool_result | done | error
58
+ }
59
+ await agent.run(session, 'generate', { intent });
60
+ ```
61
+
62
+ ## The four parts
63
+
64
+ | Part | Declared with | What the harness does |
65
+ |---|---|---|
66
+ | Prompt | `promptLayer(name, text, {cache})` | Static layers first, `cache_control` on the last cached one (Anthropic block array; joined string elsewhere) |
67
+ | Tools | `defineTool({name, inputSchema, execute\|endpoint})` | JSON-schema check at the call boundary, then the execute-and-feed-back loop (bounded by `maxToolRounds`; gate calls via `onToolCall`) |
68
+ | Workflows | `defineWorkflow(name, steps)` | Sequential cascade — `until` accepts, absent `until` is terminal, `when` skips |
69
+ | Resources | `defineResource({name, mode, get})` | `attach` → dynamic prompt layer each turn; `tool` → a read-only `read_<name>` tool |
70
+
71
+ ## Events
72
+
73
+ `agent.send()` yields the `AgentEvent` union; `reduce(session, event)` folds it.
74
+ Every failure inside a tool call becomes an `isError` `tool_result` the model
75
+ can react to — the loop throws for nothing tool-shaped.
76
+
77
+ ```
78
+ { type:'message', role:'user', text }
79
+ { type:'text', text, snapshot } · { type:'thinking', text }
80
+ { type:'tool_use', id, name, input } · { type:'tool_result', id, name, output, isError? }
81
+ { type:'step', workflow, step, data? }
82
+ { type:'progress', stage } · { type:'surface', surfaceId, line }
83
+ { type:'done', text, usage, stopReason } · { type:'error', error }
84
+ ```
85
+
86
+ `stopReason` passes through raw from the provider; the loop adds exactly one
87
+ synthetic value, `max_tool_rounds`, when the bound is hit.
88
+
89
+ `thinking` / `step` / `error` / `progress` / `surface` are render-only —
90
+ `reduce()` folds them into no session change.
91
+
92
+ ### Progress events (CHAT-HARNESS law 3)
93
+
94
+ `progress` carries a CLOSED, code-owned `stage`: one of `PROGRESS_STAGES`
95
+ (`'sent' | 'started' | 'reasoning' | 'content' | 'validating' | 'retry' |
96
+ 'tool' | 'done'`), exported alongside the `isProgressStage()` guard so any
97
+ reader — a deserialized wire message, a stored transcript — can drop an
98
+ out-of-vocabulary value instead of rendering it. **Never render `stage` as
99
+ label text directly**; a UI's label table is code-owned and keyed on these
100
+ strings, never on model output (the honesty guard).
101
+
102
+ The loop today emits only the stages it genuinely reaches — `sent` before
103
+ every provider call, `retry` before a call that repeats a round after tool
104
+ feedback, `tool` before each tool executes, `done` right before the
105
+ terminal `done` event. `started` / `reasoning` / `content` / `validating`
106
+ are declared in the vocabulary for future producers (the L1 loop doesn't
107
+ independently observe those signals today, so it doesn't invent an
108
+ emission point for them).
109
+
110
+ ### Surface events
111
+
112
+ `{ type:'surface', surfaceId, line }` carries one generative-UI wire line
113
+ for an envelope-keyed surface. This package only carries the event kind on
114
+ the union — turning it into a rendered surface is L3's job
115
+ (`web-modules/chat`, WCH-4), never this package's.
116
+
117
+ ### Framed client-message turns (CHAT-HARNESS law 6)
118
+
119
+ A surface action, a function/tool result, or a validation rejection
120
+ re-enters the agent as a *turn* — never a raw data blob — through one
121
+ framing function:
122
+
123
+ ```js
124
+ import { frameClientMessage, shouldRunTurn } from '@adia-ai/agent';
125
+
126
+ const msg = { kind: 'surfaceAction', surfaceId: 'sf_1', action: 'submit', context: { formId: 'f1' } };
127
+ if (shouldRunTurn(msg)) {
128
+ for await (const event of agent.sendClientMessage(session, msg)) { /* … */ }
129
+ }
130
+ ```
131
+
132
+ `ClientMessage` arms and their framed wording:
133
+
134
+ | Kind | Framed turn |
135
+ |---|---|
136
+ | `surfaceAction` | `The user triggered the ${action} action on surface ${surfaceId} with context ${JSON(context)}.` |
137
+ | `functionResult` | `The function ${call} returned: ${JSON(value)}.` |
138
+ | `validationRejection` | `The previous surface was rejected (${code}): ${message}. Emit a corrected surface.` |
139
+ | `dataModelUpdate`, `ack` | silent-apply — `shouldRunTurn` returns `false`; `frameClientMessage` **throws** if called on one directly |
140
+
141
+ `agent.sendClientMessage(session, msg, opts?)` is the wired entry: it gates
142
+ with `shouldRunTurn`, frames with `frameClientMessage`, then runs the same
143
+ loop as `agent.send()`. A silent-apply message yields nothing and never
144
+ touches the provider — the caller already applied it directly (e.g. to a
145
+ surface's data-model store).
146
+
147
+ ## Byte-identity baseline (CHAT-HARNESS law 2)
148
+
149
+ Every axis on `AgentConfig` is optional, and absence must produce a
150
+ byte-identical request to before the axis existed. `baseline/prompt-
151
+ equivalence.baseline.json` pins the exact `ChatOpts` a minimal config (one
152
+ static prompt layer, nothing else) produces; `baseline.test.js` re-derives
153
+ it every run and diffs byte-for-byte, including asserting no optional-axis
154
+ key (`tools`, `cache`, `signal`, …) appears at all — not even as an empty
155
+ array.
156
+
157
+ Changing a default that legitimately changes this output breaks the test
158
+ until the baseline is deliberately regenerated:
159
+
160
+ ```bash
161
+ npm run build -w @adia-ai/agent
162
+ node scripts/build/regen-agent-baseline.mjs
163
+ ```
164
+
165
+ Never hand-edit the JSON file — regenerate it, then diff-review the change
166
+ like any other pinned contract.
167
+
168
+ ## Testing
169
+
170
+ `scriptClient(turns)` is a deterministic `LLMClient`: script `[{text, toolUse}]`
171
+ turns and drive the whole loop keylessly. It also records every `ChatOpts` it
172
+ was called with (`client.calls`) so tests can assert what reached the wire.
package/agent.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * createAgent — assemble a chat agent from the four declared parts.
3
+ *
4
+ * The agent owns wiring, not state: send() streams AgentEvents through the
5
+ * tool loop, run() drives a named workflow. The caller holds the Session
6
+ * and folds events with reduce() — the agent never mutates it.
7
+ *
8
+ * System-prompt shape is provider-decided here (the one place that knows
9
+ * the provider): Anthropic gets the block array with cache_control markers,
10
+ * everyone else gets the joined string.
11
+ */
12
+ import type { ChatOpts, LLMClient, ToolUse } from '@adia-ai/llm';
13
+ import { type PromptLayer } from './prompt.js';
14
+ import { type ResourceDef } from './resource.js';
15
+ import { type Session } from './session.js';
16
+ import type { AgentEvent } from './events.js';
17
+ import { type ClientMessage } from './frame.js';
18
+ import type { ToolContext, ToolDef } from './tools.js';
19
+ import { type Workflow, type WorkflowResult } from './workflow.js';
20
+ export interface AgentConfig {
21
+ /** Client defaults (model, proxyUrl, apiKey…) handed to createClient(),
22
+ * or a ready-made client under `client` (tests: scriptClient; browser
23
+ * demos passing a client built from /packages/llm/index.js directly). */
24
+ llm: Partial<ChatOpts> | {
25
+ client: LLMClient;
26
+ model?: string;
27
+ provider?: string;
28
+ };
29
+ prompt?: PromptLayer[];
30
+ tools?: ToolDef[];
31
+ workflows?: Array<Workflow<never, never>> | Workflow[];
32
+ resources?: ResourceDef[];
33
+ maxToolRounds?: number;
34
+ /** Gate every tool call; false denies it (isError feedback, loop continues). */
35
+ onToolCall?: (call: ToolUse, ctx: ToolContext) => boolean | Promise<boolean>;
36
+ }
37
+ export interface SendOpts {
38
+ signal?: AbortSignal;
39
+ }
40
+ export interface Agent {
41
+ createSession(id?: string): Session;
42
+ send(session: Session, text: string, opts?: SendOpts): AsyncGenerator<AgentEvent>;
43
+ /** Law 6 — gates the message with `shouldRunTurn`, frames it with
44
+ * `frameClientMessage`, then runs it through the same loop as `send`.
45
+ * A silent-apply message (shouldRunTurn ⇒ false) yields nothing and
46
+ * never touches the loop — the caller already applied it directly. */
47
+ sendClientMessage(session: Session, msg: ClientMessage, opts?: SendOpts): AsyncGenerator<AgentEvent>;
48
+ run<TResult = unknown>(session: Session, workflowName: string, input?: unknown, onEvent?: (event: AgentEvent) => void): Promise<WorkflowResult<TResult>>;
49
+ /** The assembled tool set (declared + tool-mode resources) — inspectable. */
50
+ tools: ToolDef[];
51
+ }
52
+ export declare function createAgent(config: AgentConfig): Agent;
53
+ //# sourceMappingURL=agent.d.ts.map
package/agent.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["src/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAYjE,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAe,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAIhF,MAAM,WAAW,WAAW;IAC1B;;8EAE0E;IAC1E,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC;IACvD,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9E;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,aAAa,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAClF;;;2EAGuE;IACvE,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IACrG,GAAG,CAAC,OAAO,GAAG,OAAO,EACnB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GACpC,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACpC,6EAA6E;IAC7E,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAgFtD"}
package/agent.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * createAgent — assemble a chat agent from the four declared parts.
3
+ *
4
+ * The agent owns wiring, not state: send() streams AgentEvents through the
5
+ * tool loop, run() drives a named workflow. The caller holds the Session
6
+ * and folds events with reduce() — the agent never mutates it.
7
+ *
8
+ * System-prompt shape is provider-decided here (the one place that knows
9
+ * the provider): Anthropic gets the block array with cache_control markers,
10
+ * everyone else gets the joined string.
11
+ */
12
+ let _llm = null;
13
+ function llmModule() {
14
+ if (!_llm)
15
+ _llm = import('@adia-ai/llm').catch(() => null);
16
+ return _llm;
17
+ }
18
+ import { composeSystem } from './prompt.js';
19
+ import { attachLayers, resourceTools } from './resource.js';
20
+ import { createSession } from './session.js';
21
+ import { frameClientMessage, shouldRunTurn } from './frame.js';
22
+ import { runLoop } from './loop.js';
23
+ import { runWorkflow } from './workflow.js';
24
+ const DEFAULT_MAX_TOOL_ROUNDS = 8;
25
+ export function createAgent(config) {
26
+ const hasClient = 'client' in config.llm && !!config.llm.client;
27
+ const model = config.llm.model;
28
+ const explicitProvider = config.llm.provider;
29
+ let _resolved = null;
30
+ async function resolveClient() {
31
+ if (_resolved)
32
+ return _resolved;
33
+ const mod = await llmModule();
34
+ const provider = explicitProvider ?? (model ? mod?.detectProviderFromModel(model) ?? undefined : undefined);
35
+ if (hasClient) {
36
+ _resolved = { client: config.llm.client, provider };
37
+ }
38
+ else {
39
+ if (!mod)
40
+ throw new Error('@adia-ai/llm is not available — build packages/llm, or pass llm.client');
41
+ _resolved = { client: mod.createClient(config.llm), provider };
42
+ }
43
+ return _resolved;
44
+ }
45
+ const layers = config.prompt ?? [];
46
+ const resources = config.resources ?? [];
47
+ const tools = [...(config.tools ?? []), ...resourceTools(resources)];
48
+ const workflows = new Map((config.workflows ?? []).map(w => [w.name, w]));
49
+ const maxToolRounds = config.maxToolRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
50
+ async function chatOverrides(provider) {
51
+ const allLayers = [...layers, ...(await attachLayers(resources))];
52
+ if (!allLayers.length)
53
+ return {};
54
+ const composed = composeSystem(allLayers);
55
+ // Anthropic takes the block array (cache markers ride the blocks, so
56
+ // ChatOpts.cache stays off — the adapter's cache flag would re-wrap).
57
+ if (provider === 'anthropic')
58
+ return { system: composed.blocks, cache: false };
59
+ return { system: composed.text };
60
+ }
61
+ return {
62
+ tools,
63
+ createSession(id) {
64
+ return createSession(id);
65
+ },
66
+ async *send(session, text, opts = {}) {
67
+ yield { type: 'message', role: 'user', text };
68
+ const { client, provider } = await resolveClient();
69
+ const messages = [...session.messages, { role: 'user', content: text }];
70
+ const ctx = { sessionId: session.id, ...(opts.signal ? { signal: opts.signal } : {}) };
71
+ yield* runLoop(messages, {
72
+ client,
73
+ chat: await chatOverrides(provider),
74
+ tools,
75
+ maxToolRounds,
76
+ ...(config.onToolCall ? { onToolCall: config.onToolCall } : {}),
77
+ ctx,
78
+ });
79
+ },
80
+ async *sendClientMessage(session, msg, opts = {}) {
81
+ if (!shouldRunTurn(msg))
82
+ return;
83
+ const text = frameClientMessage(msg);
84
+ yield* this.send(session, text, opts);
85
+ },
86
+ async run(session, workflowName, input, onEvent) {
87
+ const workflow = workflows.get(workflowName);
88
+ if (!workflow) {
89
+ throw new Error(`Unknown workflow "${workflowName}". Declared: ${[...workflows.keys()].join(', ') || '(none)'}`);
90
+ }
91
+ return runWorkflow(workflow, {
92
+ input,
93
+ sessionId: session.id,
94
+ ...(onEvent ? { emit: (e) => onEvent(e) } : {}),
95
+ });
96
+ },
97
+ };
98
+ }
99
+ //# sourceMappingURL=agent.js.map
package/agent.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["src/agent.ts"],"names":[],"mappings":"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"}
package/events.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * AgentEvent — the one stream contract, and reduce() — the one reducer.
3
+ *
4
+ * Every surface consuming an agent renders from this union and folds
5
+ * session state with this reducer; hand-written per-surface switches over
6
+ * ad-hoc event shapes are the defect class this module exists to kill.
7
+ *
8
+ * reduce() is pure: (session, event) → new session. The loop emits events
9
+ * but never touches the caller's Session — reduce is the single writer.
10
+ */
11
+ import type { AdapterUsage } from '@adia-ai/llm';
12
+ import type { Msg, Session } from './session.js';
13
+ /** CHAT-HARNESS law 3 — the closed, code-owned progress vocabulary. Every
14
+ * reader that renders labels does so from its OWN table keyed on these
15
+ * strings — never from model text — and drops anything not in this array
16
+ * (see `isProgressStage`). Extending this list is an amendment to the
17
+ * ruled contract, not a routine addition. */
18
+ export declare const PROGRESS_STAGES: readonly ["sent", "started", "reasoning", "content", "validating", "retry", "tool", "done"];
19
+ export type ProgressStage = (typeof PROGRESS_STAGES)[number];
20
+ /** Guard for any reader (a deserialized wire message, a stored transcript,
21
+ * a future stage a newer producer emits) — out-of-vocabulary stages must
22
+ * be droppable here rather than rendered blind. */
23
+ export declare function isProgressStage(value: unknown): value is ProgressStage;
24
+ export type AgentEvent = {
25
+ type: 'message';
26
+ role: 'user';
27
+ text: string;
28
+ } | {
29
+ type: 'text';
30
+ text: string;
31
+ snapshot: string;
32
+ } | {
33
+ type: 'thinking';
34
+ text: string;
35
+ } | {
36
+ type: 'tool_use';
37
+ id: string;
38
+ name: string;
39
+ input: Record<string, unknown>;
40
+ } | {
41
+ type: 'tool_result';
42
+ id: string;
43
+ name: string;
44
+ output: string;
45
+ isError?: boolean;
46
+ } | {
47
+ type: 'step';
48
+ workflow: string;
49
+ step: string;
50
+ data?: unknown;
51
+ } | {
52
+ type: 'done';
53
+ text: string;
54
+ usage: AdapterUsage;
55
+ stopReason: string;
56
+ } | {
57
+ type: 'error';
58
+ error: Error;
59
+ }
60
+ /** Law 3 — a closed-vocabulary progress marker; render-only, no session
61
+ * effect (see reduce()). */
62
+ | {
63
+ type: 'progress';
64
+ stage: ProgressStage;
65
+ }
66
+ /** A generative-UI line for one envelope-keyed surface; render-only, no
67
+ * session effect (see reduce()). Surface HOSTING (L3, WCH-4) owns
68
+ * turning this into a rendered surface — this package only carries the
69
+ * event kind on the union. */
70
+ | {
71
+ type: 'surface';
72
+ surfaceId: string;
73
+ line: string;
74
+ };
75
+ /** Fold one event into a Session. Message-building rules:
76
+ * - `message` → append the user message.
77
+ * - `text` → update the draft's text snapshot.
78
+ * - `tool_use` → flush draft text into parts, append the tool_use part.
79
+ * - `tool_result` → close the draft as an assistant message (first result
80
+ * after tool_use parts), then append to the tool message
81
+ * (consecutive results merge into ONE tool msg — the
82
+ * Anthropic wire requires alternating roles).
83
+ * - `done` → close any remaining draft as the final assistant msg.
84
+ * - `thinking`/`step`/`error`/`progress`/`surface` → no session effect
85
+ * (render-only events — `progress`/`surface` included by the same rule:
86
+ * they narrate the turn in flight, they don't alter the transcript).
87
+ */
88
+ export declare function reduce(session: Session, event: AgentEvent): Session;
89
+ /** Convenience for the loop and tests: fold a whole event array. */
90
+ export declare function reduceAll(session: Session, events: AgentEvent[]): Session;
91
+ export type { Msg, Session };
92
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAQ,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;8CAI8C;AAC9C,eAAO,MAAM,eAAe,6FAElB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;oDAEoD;AACpD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACpF;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE;AACjC;6BAC6B;GAC3B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE;AAC5C;;;+BAG+B;GAC7B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAsEnE;AAED,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAIzE;AAED,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC"}
package/events.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * AgentEvent — the one stream contract, and reduce() — the one reducer.
3
+ *
4
+ * Every surface consuming an agent renders from this union and folds
5
+ * session state with this reducer; hand-written per-surface switches over
6
+ * ad-hoc event shapes are the defect class this module exists to kill.
7
+ *
8
+ * reduce() is pure: (session, event) → new session. The loop emits events
9
+ * but never touches the caller's Session — reduce is the single writer.
10
+ */
11
+ /** CHAT-HARNESS law 3 — the closed, code-owned progress vocabulary. Every
12
+ * reader that renders labels does so from its OWN table keyed on these
13
+ * strings — never from model text — and drops anything not in this array
14
+ * (see `isProgressStage`). Extending this list is an amendment to the
15
+ * ruled contract, not a routine addition. */
16
+ export const PROGRESS_STAGES = [
17
+ 'sent', 'started', 'reasoning', 'content', 'validating', 'retry', 'tool', 'done',
18
+ ];
19
+ /** Guard for any reader (a deserialized wire message, a stored transcript,
20
+ * a future stage a newer producer emits) — out-of-vocabulary stages must
21
+ * be droppable here rather than rendered blind. */
22
+ export function isProgressStage(value) {
23
+ return typeof value === 'string' && PROGRESS_STAGES.includes(value);
24
+ }
25
+ /** Fold one event into a Session. Message-building rules:
26
+ * - `message` → append the user message.
27
+ * - `text` → update the draft's text snapshot.
28
+ * - `tool_use` → flush draft text into parts, append the tool_use part.
29
+ * - `tool_result` → close the draft as an assistant message (first result
30
+ * after tool_use parts), then append to the tool message
31
+ * (consecutive results merge into ONE tool msg — the
32
+ * Anthropic wire requires alternating roles).
33
+ * - `done` → close any remaining draft as the final assistant msg.
34
+ * - `thinking`/`step`/`error`/`progress`/`surface` → no session effect
35
+ * (render-only events — `progress`/`surface` included by the same rule:
36
+ * they narrate the turn in flight, they don't alter the transcript).
37
+ */
38
+ export function reduce(session, event) {
39
+ switch (event.type) {
40
+ case 'message':
41
+ return {
42
+ ...session,
43
+ messages: [...session.messages, { role: 'user', content: event.text }],
44
+ };
45
+ case 'text': {
46
+ const draft = session.draft ?? { text: '', parts: [] };
47
+ return { ...session, draft: { ...draft, text: event.snapshot } };
48
+ }
49
+ case 'tool_use': {
50
+ const draft = session.draft ?? { text: '', parts: [] };
51
+ const parts = [...draft.parts];
52
+ if (draft.text && !parts.some(p => p.type === 'text')) {
53
+ parts.unshift({ type: 'text', text: draft.text });
54
+ }
55
+ parts.push({ type: 'tool_use', id: event.id, name: event.name, input: event.input });
56
+ return { ...session, draft: { text: draft.text, parts } };
57
+ }
58
+ case 'tool_result': {
59
+ const resultPart = {
60
+ type: 'tool_result',
61
+ toolUseId: event.id,
62
+ name: event.name,
63
+ content: event.output,
64
+ ...(event.isError ? { isError: true } : {}),
65
+ };
66
+ const messages = [...session.messages];
67
+ const draft = session.draft;
68
+ if (draft?.parts.length) {
69
+ messages.push({ role: 'assistant', content: draft.parts });
70
+ }
71
+ const last = messages[messages.length - 1];
72
+ if (draft?.parts.length || last?.role !== 'tool' || typeof last.content === 'string') {
73
+ messages.push({ role: 'tool', content: [resultPart] });
74
+ }
75
+ else {
76
+ messages[messages.length - 1] = { role: 'tool', content: [...last.content, resultPart] };
77
+ }
78
+ const next = { ...session, messages };
79
+ delete next.draft;
80
+ return next;
81
+ }
82
+ case 'done': {
83
+ const messages = [...session.messages];
84
+ const draft = session.draft;
85
+ if (draft?.parts.length) {
86
+ messages.push({ role: 'assistant', content: draft.parts });
87
+ }
88
+ else if (event.text || draft?.text) {
89
+ messages.push({ role: 'assistant', content: event.text || draft?.text || '' });
90
+ }
91
+ const next = { ...session, messages };
92
+ delete next.draft;
93
+ return next;
94
+ }
95
+ case 'thinking':
96
+ case 'step':
97
+ case 'error':
98
+ case 'progress':
99
+ case 'surface':
100
+ return session;
101
+ default:
102
+ return session;
103
+ }
104
+ }
105
+ /** Convenience for the loop and tests: fold a whole event array. */
106
+ export function reduceAll(session, events) {
107
+ let next = session;
108
+ for (const event of events)
109
+ next = reduce(next, event);
110
+ return next;
111
+ }
112
+ //# sourceMappingURL=events.js.map
package/events.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["src/events.ts"],"names":[],"mappings":"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"}
package/frame.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * frameClientMessage / shouldRunTurn — law 6: framed client-message turns.
3
+ *
4
+ * The session is a plain serializable turn array (session.ts); every
5
+ * surface action, function/tool result, and validation rejection re-enters
6
+ * the loop as a *distinct natural-language user turn* through this ONE
7
+ * framing function — never scattered ad-hoc string-building at call sites.
8
+ *
9
+ * Silent-apply kinds (a data-model echo, an action-response ack the caller
10
+ * already consumed) never construct a turn at all: `shouldRunTurn` gates
11
+ * them out BEFORE framing is attempted, and `frameClientMessage` itself
12
+ * THROWS if handed one directly — a should-not-run message is
13
+ * unconstructable as a turn, not merely skipped. This mirrors the wire's
14
+ * own three-arm client vocabulary (action / error / functionResponse) one
15
+ * level up, in plain language the model reads as a user turn.
16
+ */
17
+ export type ClientMessage =
18
+ /** A surface action fired (a button, a form submit) — carries the
19
+ * action's declared context, already resolved off the surface's store. */
20
+ {
21
+ kind: 'surfaceAction';
22
+ surfaceId: string;
23
+ action: string;
24
+ context?: unknown;
25
+ }
26
+ /** A model-callable function returned its result. */
27
+ | {
28
+ kind: 'functionResult';
29
+ call: string;
30
+ value: unknown;
31
+ }
32
+ /** A previously emitted surface failed validation — the harness asks for
33
+ * a corrected one. */
34
+ | {
35
+ kind: 'validationRejection';
36
+ code: string;
37
+ message: string;
38
+ }
39
+ /** Silent-apply: a data-model value changed and was already applied to
40
+ * the surface's store directly — no narration, no turn. */
41
+ | {
42
+ kind: 'dataModelUpdate';
43
+ surfaceId: string;
44
+ path?: string;
45
+ value: unknown;
46
+ }
47
+ /** Silent-apply: an action-response the caller already resolved off its
48
+ * own promise — nothing left to narrate. */
49
+ | {
50
+ kind: 'ack';
51
+ actionId: string;
52
+ };
53
+ /** Gate BEFORE framing: false means this message applies silently and must
54
+ * never reach `frameClientMessage`. */
55
+ export declare function shouldRunTurn(msg: ClientMessage): boolean;
56
+ /** Maps one client-message arm to its pinned natural-language user turn.
57
+ * Deterministic — same input, same string, always — so golden tests can
58
+ * pin the exact wording. Throws for any kind `shouldRunTurn` rejects. */
59
+ export declare function frameClientMessage(msg: ClientMessage): string;
60
+ //# sourceMappingURL=frame.d.ts.map
package/frame.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["src/frame.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,aAAa;AACvB;2EAC2E;AACzE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;AACjF,qDAAqD;GACnD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAC1D;uBACuB;GACrB;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAChE;4DAC4D;GAC1D;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAC/E;6CAC6C;GAC3C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAItC;wCACwC;AACxC,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAEzD;AAED;;0EAE0E;AAC1E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAsB7D"}