agentic-assistant 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Added
6
+
7
+ - AI transparency. Each tool result whose tool ran, and each run that got a model answer, carries a `provenance` record with `generator: "ai"`, model, provider, time, assistant id, the person it acts for, the tool and its risk, and the approver with a `kind`. A denied call has no record.
8
+ - `createProvenance`, `provenanceMarker` (JSON-LD with the IPTC `trainedAlgorithmicMedia` source type) and `provenanceLabel`.
9
+ - The record is in SDK events, the HTTP chat stream, `--json` output of the CLI, and `assistant_chat` results of the MCP server. The MCP result also holds `label` and `marker`. The CLI prints the label after a run.
10
+ - Approval decisions may carry `approverKind` (`human`, `agent`, or `host`). A decision without it is recorded as `unknown`. `QueueApprovalGate.approve(id, decidedBy, approverKind)` takes it as a third argument: pass it, or every record shows `unknown`. The CLI sets `host` for a pre-approved call and `human` for a typed yes. The HTTP API sets `human` for a person and `agent` for any other caller.
11
+
12
+ Changed
13
+
14
+ - A provider that gives an empty model or provider name now ends the run with `provider_error` before any tool runs.
15
+
16
+ ## 0.1.0
17
+
18
+ First release.
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
 
8
8
  An AI assistant that acts inside your apps with the user's own permissions.
9
9
 
10
- > Status: version 0.1.0, the first release. The core loop, the tool sources, approvals, and the React panel work.
11
- > The chat CLI and the MCP server are not built yet. The `agentic-assistant` command only prints help and version.
10
+ > Status: early. The core loop, the tool sources, approvals, the React panel, the CLI, and the MCP server work.
11
+ > There is no hosted HTTP API yet.
12
12
 
13
13
  ## Install
14
14
 
@@ -120,6 +120,99 @@ The panel streams the answer, shows each tool call as a card, and shows an appro
120
120
 
121
121
  The look comes from plain CSS variables (`--aa-bg`, `--aa-fg`, `--aa-accent`, and others). Set them on `.aa-panel` or a parent. The sheet loads no font, image, or file from another host. A prompt that the run left open (after Stop or an error) is closed and marked expired. Closing the panel stops the run.
122
122
 
123
+ ## Use the CLI
124
+
125
+ ```sh
126
+ export ANTHROPIC_API_KEY=...
127
+ agentic-assistant chat \
128
+ --tools https://notes.example.eu/mcp --tools-header-env authorization=NOTES_TOKEN \
129
+ --scope notes:read --tool-scope notes:read --tool-risk safe \
130
+ --use-default-endpoint \
131
+ "What does note 7 say?"
132
+ ```
133
+
134
+ The assistant prints its answer on stdout. The answer starts with `Assistant (machine-made):`. Tool calls and approval requests on stderr start with `[machine-made]`, and so does the question that `--approve ask` shows. Control characters are removed from all text it prints for a person, so a tool cannot move your cursor or change your terminal title. `--json` events are not changed: they carry the `actor` with `machineMade: true` on each action, and the text events belong to the labeled run. Add `--json` to get one event per line on stdout: that is the form for agents and tests. The exit code is 0 when the run ends normally, 1 when it stops for another reason (a cap, an error), and 2 for a usage error.
135
+
136
+ - `--tools` takes an `https` MCP URL, or a command that starts a server on stdio (words split on spaces, or a JSON list like `'["node","server.js"]'`).
137
+ - `--scope` is what the assistant may hold. `--tool-scope` is what each tool needs. Both are required, and the CLI guesses neither.
138
+ - `--tool-risk` defaults to `risky`. A risky call needs approval. Approve one exact call ahead of time with `--approve-call '{"tool":"notes_write","input":{"id":"7","text":"hi"}}'`. The call must match the tool and the arguments, and each approval is used once. On a terminal, `--approve ask` asks you.
139
+ - Keys come from the environment: `ANTHROPIC_API_KEY`, or the variable named by `--api-key-env`. There is no flag that takes a key.
140
+ - For an EU-hosted model, use `--provider openai-compatible --base-url https://models.example.eu/v1 --model <id> --api-key-env EU_KEY`. Add `--price-in` and `--price-out` to use the cost cap.
141
+ - A message on stdin may be up to 1 MB.
142
+ - Limits: `--max-turns`, `--max-tokens`, `--max-cost`, `--max-tool-calls`.
143
+ - `--provider scripted --script turns.json` replays fixed turns. It is for tests.
144
+
145
+ ## Run it as an MCP server
146
+
147
+ ```sh
148
+ agentic-assistant mcp --tools https://notes.example.eu/mcp --scope notes:read --tool-scope notes:read --tool-risk safe --use-default-endpoint
149
+ ```
150
+
151
+ Add this to an MCP client as a stdio server. It offers one tool, `assistant_chat`, with one argument, `message`. Each call starts a new conversation. The reply is JSON with `machineMade: true`, the answer, the stop reason, and the tool calls. A run that stops on a cap is an error result. The caps (`--max-turns`, `--max-cost`) apply to one call, and the server has no total limit, so set a limit in the client that calls it. The server stops when the client closes its input. A call that is still running is cancelled and dropped: the client gets no reply for it. The tool servers are closed. The server cannot ask a person, so a risky call runs only when you started the server with a matching `--approve-call`.
152
+
153
+ ## Run it as a hosted HTTP API
154
+
155
+ ```ts
156
+ // src/index.ts of your Worker. `store`, `registry`, and `Env` are yours.
157
+ import { createWorker } from "agentic-assistant/worker";
158
+ import { AnthropicProvider } from "agentic-assistant";
159
+ import { createStoreAuth } from "agentic-identity";
160
+
161
+ let worker: ReturnType<typeof createWorker> | undefined;
162
+
163
+ export default {
164
+ fetch(request: Request, env: Env) {
165
+ // Build the worker once. It keeps the pending approvals, so a new one per request would lose them.
166
+ worker ??= createWorker({
167
+ auth: createStoreAuth(store(env)),
168
+ registry,
169
+ provider: () => new AnthropicProvider({ baseUrl: env.MODEL_URL, apiKey: env.MODEL_KEY }),
170
+ });
171
+ return worker.fetch(request, env);
172
+ },
173
+ };
174
+ ```
175
+
176
+ Every route is under `/api/v1` and needs `Authorization: Bearer <key>`. The key resolves to an `agentic-identity` principal. A person, an agent, and a token that acts for a user all work. A missing, bad, or odd credential is a 401.
177
+
178
+ | Route | What it does |
179
+ |---|---|
180
+ | `GET /tools` | The tools inside the caller's scopes: name, description, scope, risk, input schema. |
181
+ | `POST /chat` `{"message":"..."}` | One new conversation. The answer is a stream of server-sent events (`event: <type>`, `data: <json>`), the same events as `runAssistant`, ending with `done`. |
182
+ | `GET /approvals` | Pending risky calls of the same owner, with the digest and `canDecide`. |
183
+ | `POST /approvals/:id/approve` `{"digest":"..."}` | Approve the exact call. A wrong digest is a 409. |
184
+ | `POST /approvals/:id/deny` `{"reason":"..."}` | Refuse the call. |
185
+
186
+ The assistant holds the caller's scopes and no more. A chat turn waits at a risky call until another principal decides. The stream stays open for that time. A principal that asked for the turn never decides on its own proposal: an agent gets a 403 for both approve and deny. The same holds for an agent it made and for the agent that made it. A person who asks may decide, because the assistant proposed the call. Another agent may decide only when it holds `assistant:admin` and the scope of the tool. A principal with another owner sees a 404. The list hides a call whose tool the caller has no scope for. If a client hangs up, the run stops and its pending calls go. If the key is revoked or its scopes change during a run, the run stops before its next tool call.
187
+
188
+ ## AI transparency
189
+
190
+ The package marks what the assistant makes as AI-generated. This follows the idea of EU AI Act Article 50.
191
+
192
+ Each tool result and each finished run carries a `provenance` record: `generator: "ai"`, the model, the provider, the time, the assistant id, the person it acts for, and the tool, its risk, and the approver when someone approved the call. The approver has a `kind`: `human`, `agent`, `host` (a rule you set), or `unknown`. Say "a person approved" only for `human`. After a fallback, the record names the model that answered. A denied call has no record, because nothing ran.
193
+
194
+ ```ts
195
+ import { provenanceLabel, provenanceMarker } from "agentic-assistant";
196
+
197
+ for await (const event of runAssistant(options)) {
198
+ if (event.type === "tool_result" && event.provenance) {
199
+ provenanceLabel(event.provenance); // "AI-generated by model-1 (acme), approved by ana"
200
+ provenanceMarker(event.provenance); // JSON-LD with the IPTC trainedAlgorithmicMedia source type
201
+ }
202
+ }
203
+ ```
204
+
205
+ The record is in the SDK events, in the HTTP chat stream, in `--json` output of the CLI, and in the `assistant_chat` result of the MCP server. The MCP result also holds `label` and `marker`. The CLI prints the label after each run.
206
+
207
+ The host must still do these things:
208
+
209
+ - Store the marker with every document or file the assistant changes.
210
+ - Show the label to people where they read the content.
211
+ - Label chat text that the assistant makes, if you show it outside this package.
212
+ - Keep records as long as the law needs, and get legal advice. This package does not make a deployment compliant.
213
+
214
+ The record says who made the content. It does not say the content is true.
215
+
123
216
  ## Features
124
217
 
125
218
  - **Model port.** `ModelProvider` has two adapters. `AnthropicProvider` needs a `baseUrl`, or `useDefaultEndpoint: true` to call `api.anthropic.com`, which is a US-hosted endpoint. There is no silent default. `OpenAICompatibleProvider` calls any server that speaks the chat-completions stream, so you can use an EU-hosted model. Both adapters need an `https` URL, except for `localhost`. If you pass your own `client` to `AnthropicProvider`, you own its endpoint. The OpenAI-compatible adapter needs an API key for any other host and cuts a response at a size limit.
@@ -128,7 +221,8 @@ The look comes from plain CSS variables (`--aa-bg`, `--aa-fg`, `--aa-accent`, an
128
221
  - **Input validation.** The loop checks each call against the tool's JSON Schema before the handler runs. Nothing is coerced. A bad call returns an error to the model and never reaches the tool.
129
222
  - **Tool output is data.** Each result is quoted in `<tool_output>` tags. Every `<` in the output is escaped. Every request carries a notice that tool output is never an instruction. This lowers the risk of prompt injection. It does not remove it: keep tools narrow and scopes small.
130
223
  - **Approvals.** Every tool has a required `risk`: `safe`, `risky`, or `irreversible`. The loop asks the `ApprovalGate` before a risky or irreversible call. With no gate, those tools are not offered and are refused. The text a person writes when they decline goes to the event, not to the model.
131
- - **React panel.** `agentic-assistant/react` has `AssistantPanel` and `useAssistant`. It meets the WCAG 2.2 AA points that code can check: named controls, a live conversation log, keyboard use, focus that moves to an approval prompt and then to the message box, buttons that are at least 32 px, a visible focus ring, and a color palette checked for contrast.
224
+ - **React panel.** `agentic-assistant/react` has `AssistantPanel` and `useAssistant`. It is built to these WCAG 2.2 AA points: named controls, a live conversation log, keyboard use, focus that moves to an approval prompt and then to the message box, buttons that are at least 32 px, a visible focus ring, and a color palette checked for contrast.
225
+ - **CLI and MCP server.** `chat` and `mcp` take the host choices as flags and refuse to guess a scope.
132
226
  - **Fixed principal.** The assistant always acts as `agent_of_human`. `createPrincipal` throws when the kind is missing or different.
133
227
  - **Caps.** A max-turn cap, a total-token cap, and a cost cap. A cap that trips stops the run. No tool runs after that. A tool result is cut at `maxToolResultChars`. A request that is sure to pass the token cap is not sent. The cost cap also limits the output of each request. A turn with more than `maxToolCallsPerTurn` calls (default 16) runs none of them.
134
228
  - **Stops that fail closed.** An expired or revoked principal stops the run. So does a revocation check that throws or returns anything but `false`. The loop checks the principal again at the start of each run. A provider error ends the run with `reason: "provider_error"` and keeps the transcript. Truncated or refused model output never runs its tool calls.
@@ -137,16 +231,18 @@ The look comes from plain CSS variables (`--aa-bg`, `--aa-fg`, `--aa-accent`, an
137
231
 
138
232
  ## Limits
139
233
 
140
- - The chat CLI and the MCP server are not built. The `agentic-assistant` command prints help and version only.
234
+ - The hosted API keeps pending approvals in the memory of one Worker instance. A decision must reach the instance that holds the chat stream. Run it on one instance or behind sticky routing until a shared queue exists. It has no per-IP limit, only a cap of four chat turns at once per principal.
235
+ - The tests run the API under Node with a fake model. They do not run it under `wrangler dev`. The sample Worker in `wrangler.jsonc` answers `/health` only.
236
+ - The CLI reads one message, or a short conversation on a terminal. It does not keep a conversation between runs.
141
237
  - The panel is tested in a simulated browser (jsdom). Contrast is checked from the CSS values. Run a screen reader and a real browser before you ship it to people who need it.
142
238
  - The panel shows plain text, not Markdown.
143
239
  - The gate trusts the name that `decidedBy` carries. Your gate must check who the person is. `QueueApprovalGate` does not.
144
240
  - An MCP server is trusted for its own output only. The assistant does not sandbox the server process. Run stdio servers you trust.
145
241
  - Token and cost caps need the provider to report usage. The OpenAI-compatible adapter throws when the server sends none.
146
- - A cost is an estimate from the `pricing` you give. Cache discounts are not applied. The built-in Anthropic prices are from 2026-10-06. Check them.
242
+ - A cost is an estimate from the `pricing` you give. Cache discounts are not applied. The built-in Anthropic prices were checked on 2026-10-29 (`PRICING_CHECKED_ON`). A test fails when that date is over 120 days old. Haiku has no built-in price, because its rate changes with the prompt size. Pass `pricing` for it. Check the prices before you rely on a cap.
147
243
  - The token pre-check guesses three characters per token. It is an estimate.
148
244
  - A tool error text goes to the `detail` field of the event. The model sees only `tool failed`.
149
- - A model refusal stops the run. No automatic fallback model is set up.
245
+ - A model refusal stops the run, unless you set a fallback model. `FallbackProvider(primary, fallback)` or `--fallback-model <id>` asks the second model once when the first refuses or fails. Each `turn_end` event has `answeredBy` with the model that answered. The text of the first model is held until its turn ends, so a turn with a fallback does not stream word by word. A refusal from the fallback stops the run. The fallback gets only the output room that the first ask left, and the caps count the input of both asks, so a token or cost cap holds. A first ask that failed after it streamed is counted by an estimate. If the fallback itself fails after it streamed, the totals of that run leave out the first ask. Its error text is not kept.
150
246
 
151
247
  ## Develop
152
248
 
@@ -155,6 +251,12 @@ npm ci
155
251
  npm run ci:local
156
252
  ```
157
253
 
254
+ The live smoke test calls the real Anthropic API. It costs a few cents, so it runs only when you ask:
255
+
256
+ ```sh
257
+ AGENTIC_ASSISTANT_LIVE=1 ANTHROPIC_API_KEY=... npx vitest run src/live.smoke.test.ts
258
+ ```
259
+
158
260
  ## License
159
261
 
160
262
  MIT.
package/SKILL.md CHANGED
@@ -27,6 +27,23 @@ registry.registerAll(mcp.tools);
27
27
 
28
28
  Give each tool a `risk`. Pass `approvals: new QueueApprovalGate()` to `runAssistant`. On an `approval_request` event, show the exact arguments. Then call `approve(id, personId)` or `reject(id, personId)`.
29
29
 
30
+ ## Serve the HTTP API
31
+
32
+ ```ts
33
+ import { createWorker } from "agentic-assistant/worker";
34
+ const worker = createWorker({ auth, registry, provider: () => model });
35
+ ```
36
+
37
+ Routes: `GET /api/v1/tools`, `POST /api/v1/chat` (server-sent events), `GET /api/v1/approvals`, `POST /api/v1/approvals/:id/approve` with the digest, and `/deny`. The caller that asked for a turn never decides on it, if it is an agent.
38
+
39
+ ## Use the CLI
40
+
41
+ ```sh
42
+ agentic-assistant chat --tools <mcp-url> --scope notes:read --tool-scope notes:read --tool-risk safe --json "message"
43
+ ```
44
+
45
+ Set the key in the environment. Approve one exact risky call with `--approve-call`. Read the exit code: 0 normal, 1 stopped or failed, 2 usage.
46
+
30
47
  ## Show a panel
31
48
 
32
49
  ```tsx
@@ -0,0 +1,120 @@
1
+ import { S as Scope, u as ToolRisk, s as ToolDefinition, R as RunLimits, t as ToolRegistry, d as ApprovalGate, h as AssistantEvent } from './loop-42giKLVa.js';
2
+ import { P as Pricing, M as ModelProvider, c as Message } from './types-DGH8FS56.js';
3
+
4
+ type McpTransportConfig = {
5
+ type: "stdio";
6
+ command: string;
7
+ args?: string[];
8
+ env?: Record<string, string>;
9
+ cwd?: string;
10
+ } | {
11
+ type: "http";
12
+ url: string;
13
+ headers?: Record<string, string>;
14
+ };
15
+ interface McpToolInfo {
16
+ name: string;
17
+ description?: string;
18
+ annotations?: {
19
+ readOnlyHint?: boolean;
20
+ destructiveHint?: boolean;
21
+ idempotentHint?: boolean;
22
+ openWorldHint?: boolean;
23
+ };
24
+ }
25
+ interface ConnectMcpOptions {
26
+ transport: McpTransportConfig;
27
+ /** The scope each tool needs. There is no default. A function can choose per tool. */
28
+ scope: Scope | ((tool: McpToolInfo) => Scope);
29
+ /** How risky each tool is. There is no default. A function can choose per tool. Do not trust the server's hints. */
30
+ risk: ToolRisk | ((tool: McpToolInfo) => ToolRisk);
31
+ /** Put before each tool name, to keep names from two servers apart. */
32
+ prefix?: string;
33
+ /** Most characters of tool output to keep. Default 32768. */
34
+ maxOutputChars?: number;
35
+ /** Most tools to load. Default 200. */
36
+ maxTools?: number;
37
+ /** Longest tool description to pass to the model. Default 1000. */
38
+ maxDescriptionChars?: number;
39
+ /** Time to wait for one tool call. Default 30000. */
40
+ callTimeoutMs?: number;
41
+ /** Time to connect and list every tool, all pages together. Default 30000. */
42
+ loadTimeoutMs?: number;
43
+ /** Most bytes to read from one HTTP response. Default 4000000. Stdio is limited by the SDK. */
44
+ maxResponseBytes?: number;
45
+ }
46
+ interface McpTools {
47
+ tools: ToolDefinition[];
48
+ /** Tools that were not loaded, with the reason. */
49
+ skipped: {
50
+ name: string;
51
+ reason: string;
52
+ }[];
53
+ close(): Promise<void>;
54
+ }
55
+ /**
56
+ * Connects to an MCP server and turns its tools into assistant tools. The host
57
+ * names the scope each tool needs. Output text is capped, and non-text content is
58
+ * left out. Tools with a bad name, schema, or scope are skipped, not loaded.
59
+ */
60
+ declare function connectMcpTools(options: ConnectMcpOptions): Promise<McpTools>;
61
+
62
+ declare class ConfigError extends Error {
63
+ constructor(message: string);
64
+ }
65
+ interface ChatConfig {
66
+ /** An http(s) URL of an MCP server, or a command line that starts one on stdio. */
67
+ tools: string;
68
+ /** Names of environment variables that hold HTTP headers for the MCP server: `header=ENV_NAME`. */
69
+ toolHeaderEnv: string[];
70
+ scopes: string[];
71
+ toolScope: string;
72
+ toolRisk: ToolRisk;
73
+ provider: "anthropic" | "openai-compatible" | "scripted";
74
+ model?: string;
75
+ /** A second model on the same provider. It answers once when the first one refuses or fails. */
76
+ fallbackModel?: string;
77
+ baseUrl?: string;
78
+ useDefaultEndpoint: boolean;
79
+ apiKeyEnv?: string;
80
+ pricing?: Pricing;
81
+ script?: string;
82
+ system?: string;
83
+ user: string;
84
+ limits: RunLimits;
85
+ /** Exact calls a person approved before the run. Each is used once. */
86
+ preApproved: {
87
+ tool: string;
88
+ input: unknown;
89
+ }[];
90
+ approve: "deny" | "ask";
91
+ }
92
+ interface ChatIo {
93
+ env: Record<string, string | undefined>;
94
+ readFile(path: string): string;
95
+ /** Ask a person a question and return the answer. Only used for `--approve ask`. */
96
+ ask?(question: string): Promise<string>;
97
+ }
98
+ interface Setup {
99
+ provider: ModelProvider;
100
+ registry: ToolRegistry;
101
+ gate: ApprovalGate;
102
+ principalOptions: {
103
+ id: string;
104
+ parentId: string;
105
+ scopes: string[];
106
+ };
107
+ limits: RunLimits;
108
+ system?: string;
109
+ skipped: {
110
+ name: string;
111
+ reason: string;
112
+ }[];
113
+ close(): Promise<void>;
114
+ }
115
+ /** An http(s) URL is an HTTP server. A JSON array of strings, or words split on spaces, is a command. */
116
+ declare function parseTarget(target: string, headerEnv: string[], env: ChatIo["env"]): McpTransportConfig;
117
+ declare function buildSetup(config: ChatConfig, io: ChatIo): Promise<Setup>;
118
+ declare function runChat(setup: Setup, messages: Message[], signal?: AbortSignal): AsyncGenerator<AssistantEvent>;
119
+
120
+ export { type ChatConfig as C, type McpToolInfo as M, type Setup as S, type ChatIo as a, ConfigError as b, type ConnectMcpOptions as c, type McpTools as d, type McpTransportConfig as e, buildSetup as f, connectMcpTools as g, parseTarget as p, runChat as r };
@@ -0,0 +1,82 @@
1
+ import {
2
+ runChat
3
+ } from "./chunk-BDXAUNMR.js";
4
+ import {
5
+ VERSION,
6
+ provenanceLabel,
7
+ provenanceMarker
8
+ } from "./chunk-T4ZMDOB4.js";
9
+
10
+ // src/mcp-server.ts
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
+ var MAX_MESSAGE_CHARS = 1e4;
15
+ var TOOL = {
16
+ name: "assistant_chat",
17
+ description: "Send one message to the assistant. It acts for a person with the scopes the host gave it. A risky action runs only if the host pre-approved that exact call. The reply says it is machine-made.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: { message: { type: "string", minLength: 1, maxLength: MAX_MESSAGE_CHARS, description: "What to ask the assistant." } },
21
+ required: ["message"],
22
+ additionalProperties: false
23
+ },
24
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
25
+ };
26
+ var fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
27
+ function createAssistantMcpServer(setup, shutdown) {
28
+ const server = new Server({ name: "agentic-assistant", version: VERSION }, { capabilities: { tools: {} } });
29
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [TOOL] }));
30
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
31
+ if (request.params.name !== TOOL.name) return fail(`unknown tool "${request.params.name}"`);
32
+ const message = request.params.arguments?.message;
33
+ if (typeof message !== "string" || message.trim() === "" || message.length > MAX_MESSAGE_CHARS) {
34
+ return fail(`message must be a text of 1 to ${MAX_MESSAGE_CHARS} characters`);
35
+ }
36
+ const call = new AbortController();
37
+ const stop = () => call.abort();
38
+ extra.signal.addEventListener("abort", stop, { once: true });
39
+ shutdown?.addEventListener("abort", stop, { once: true });
40
+ if (extra.signal.aborted || shutdown?.aborted) stop();
41
+ let reply = "";
42
+ const toolCalls = [];
43
+ let outcome;
44
+ for await (const event of runChat(setup, [{ role: "user", content: [{ type: "text", text: message }] }], call.signal)) {
45
+ if (event.type === "text_delta") reply += event.text;
46
+ else if (event.type === "turn_start") reply = "";
47
+ else if (event.type === "tool_result") toolCalls.push({ name: event.name, isError: event.isError, ...event.denied ? { denied: event.denied } : {}, ...event.provenance ? { provenance: event.provenance } : {} });
48
+ else if (event.type === "done") outcome = { reason: event.reason, totals: event.totals, provenance: event.provenance };
49
+ }
50
+ extra.signal.removeEventListener("abort", stop);
51
+ shutdown?.removeEventListener("abort", stop);
52
+ const provenance = outcome?.provenance;
53
+ const result = {
54
+ machineMade: true,
55
+ reply,
56
+ reason: outcome?.reason ?? "unknown",
57
+ toolCalls,
58
+ totals: outcome?.totals,
59
+ ...provenance ? { provenance, label: provenanceLabel(provenance), marker: provenanceMarker(provenance) } : {}
60
+ };
61
+ return { ...outcome?.reason === "end_turn" ? {} : { isError: true }, content: [{ type: "text", text: JSON.stringify(result) }] };
62
+ });
63
+ return server;
64
+ }
65
+ async function startAssistantMcp(setup) {
66
+ const shutdown = new AbortController();
67
+ const server = createAssistantMcpServer(setup, shutdown.signal);
68
+ const ended = new Promise((resolve) => {
69
+ server.onclose = resolve;
70
+ process.stdin.once("end", resolve);
71
+ process.stdin.once("close", resolve);
72
+ });
73
+ await server.connect(new StdioServerTransport());
74
+ await ended;
75
+ shutdown.abort();
76
+ await server.close().catch(() => void 0);
77
+ }
78
+
79
+ export {
80
+ createAssistantMcpServer,
81
+ startAssistantMcp
82
+ };