flint-agent 1.14.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 (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,167 @@
1
+ // The stream-json protocol of the stdio mode: one JSON object per line on
2
+ // stdin and on stdout, in the common stream-json shapes, so a host written for
3
+ // `claude --input-format stream-json --output-format stream-json` reads Flint
4
+ // without changes. A host driving it reads:
5
+ //
6
+ // assistant message.content[] text and tool_use blocks, message.usage, message.id
7
+ // result subtype ("success" or an error), result, usage, total_cost_usd
8
+ // error message (a turn that could not run at all)
9
+ //
10
+ // and writes `user` messages and `control_request` interrupts. Everything else
11
+ // (system init, tool results, control responses) is for other hosts and logs.
12
+ //
13
+ // Pure functions only; the loop that uses them is stdio/run.js.
14
+
15
+ /** A tool result line is for logs; the model already has the full text. */
16
+ export const TOOL_RESULT_MAX_CHARS = 16000;
17
+
18
+ /** Parse one input line. Null for a blank or non-JSON line, which is skipped. */
19
+ export function parseInputLine(line) {
20
+ const s = String(line || "").trim();
21
+ if (!s) return null;
22
+ try {
23
+ const v = JSON.parse(s);
24
+ return v && typeof v === "object" ? v : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The content of a `user` envelope as Flint's processMessage takes it: a
32
+ * string when it is only text, else OpenAI-style parts with images as data
33
+ * URLs. Null when the envelope carries nothing to answer.
34
+ */
35
+ export function userContent(envelope) {
36
+ const content = envelope?.message?.content;
37
+ if (typeof content === "string") return content.trim() ? content : null;
38
+ if (!Array.isArray(content)) return null;
39
+ const parts = [];
40
+ for (const b of content) {
41
+ if (!b || typeof b !== "object") continue;
42
+ if (b.type === "text" && b.text) parts.push({ type: "text", text: String(b.text) });
43
+ else if (b.type === "image" && b.source?.type === "base64" && b.source.data) {
44
+ parts.push({ type: "image_url", image_url: { url: `data:${b.source.media_type || "image/png"};base64,${b.source.data}` } });
45
+ } else if (b.type === "image" && b.source?.type === "url" && b.source.url) {
46
+ parts.push({ type: "image_url", image_url: { url: b.source.url } });
47
+ }
48
+ }
49
+ if (!parts.length) return null;
50
+ if (parts.every((p) => p.type === "text")) return parts.map((p) => p.text).join("\n\n");
51
+ return parts;
52
+ }
53
+
54
+ function toolInput(args) {
55
+ if (args && typeof args === "object") return args;
56
+ try {
57
+ const v = JSON.parse(args || "{}");
58
+ return v && typeof v === "object" ? v : { value: v };
59
+ } catch {
60
+ return { raw: String(args) };
61
+ }
62
+ }
63
+
64
+ function usageOf(usage) {
65
+ return {
66
+ input_tokens: usage?.prompt_tokens || 0,
67
+ output_tokens: usage?.completion_tokens || 0,
68
+ cache_read_input_tokens: usage?.prompt_tokens_details?.cached_tokens || 0,
69
+ cache_creation_input_tokens: 0,
70
+ };
71
+ }
72
+
73
+ export function initEvent({ sessionId, model, cwd, tools = [], mcpServers = [], permissionMode, version }) {
74
+ return {
75
+ type: "system",
76
+ subtype: "init",
77
+ session_id: sessionId,
78
+ cwd,
79
+ model,
80
+ tools,
81
+ mcp_servers: mcpServers,
82
+ permissionMode,
83
+ agent: "flint",
84
+ version,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * One model reply as an `assistant` message: its text, then one tool_use block
90
+ * per tool call. Null for a reply with neither.
91
+ */
92
+ export function assistantEvent({ reply, usage, model, sessionId, messageId }) {
93
+ const content = [];
94
+ const text = typeof reply?.content === "string" ? reply.content.trim() : "";
95
+ if (text) content.push({ type: "text", text });
96
+ for (const tc of reply?.tool_calls || []) {
97
+ content.push({ type: "tool_use", id: tc.id, name: tc.function?.name, input: toolInput(tc.function?.arguments) });
98
+ }
99
+ if (!content.length) return null;
100
+ return {
101
+ type: "assistant",
102
+ message: {
103
+ id: messageId,
104
+ type: "message",
105
+ role: "assistant",
106
+ model,
107
+ content,
108
+ stop_reason: reply?.tool_calls?.length ? "tool_use" : "end_turn",
109
+ usage: usageOf(usage),
110
+ },
111
+ parent_tool_use_id: null,
112
+ session_id: sessionId,
113
+ };
114
+ }
115
+
116
+ export function toolResultEvent({ toolUseId, result, isError, sessionId }) {
117
+ let text = typeof result === "string" ? result : JSON.stringify(result);
118
+ if (text == null) text = "";
119
+ if (text.length > TOOL_RESULT_MAX_CHARS) {
120
+ text = text.slice(0, TOOL_RESULT_MAX_CHARS) + `\n[... ${text.length - TOOL_RESULT_MAX_CHARS} more characters]`;
121
+ }
122
+ return {
123
+ type: "user",
124
+ message: { role: "user", content: [{ type: "tool_result", tool_use_id: toolUseId, content: text, is_error: !!isError }] },
125
+ parent_tool_use_id: null,
126
+ session_id: sessionId,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * The end of a turn. `subtype` is "success", "error_during_execution" (the
132
+ * turn failed or was interrupted) or "error_max_turns" (the step limit).
133
+ */
134
+ export function resultEvent({ subtype, text, sessionId, durationMs, numTurns, costUsd, promptTokens, completionTokens, stopReason }) {
135
+ return {
136
+ type: "result",
137
+ subtype,
138
+ is_error: subtype !== "success",
139
+ duration_ms: durationMs,
140
+ num_turns: numTurns,
141
+ result: text || "",
142
+ session_id: sessionId,
143
+ total_cost_usd: costUsd || 0,
144
+ usage: { input_tokens: promptTokens || 0, output_tokens: completionTokens || 0 },
145
+ stop_reason: stopReason || null,
146
+ };
147
+ }
148
+
149
+ export function controlResponse(requestId, extra = {}) {
150
+ return { type: "control_response", response: { subtype: "success", request_id: requestId, response: extra } };
151
+ }
152
+
153
+ export function controlError(requestId, error) {
154
+ return { type: "control_response", response: { subtype: "error", request_id: requestId, error } };
155
+ }
156
+
157
+ /**
158
+ * Which result subtype a finished processMessage maps to. Flint's own stop
159
+ * reasons (agent/agent.js): "done"; "denied" (it stopped asking after
160
+ * repeated denials and said so, an answer like any other); "budget" (the
161
+ * cost ceiling); "error", "stall", "empty" (the turn did not get anywhere).
162
+ */
163
+ export function resultSubtype(stopReason) {
164
+ if (!stopReason || stopReason === "done" || stopReason === "denied") return "success";
165
+ if (stopReason === "budget") return "error_max_budget_usd";
166
+ return "error_during_execution";
167
+ }
@@ -0,0 +1,106 @@
1
+ // The stdio mode: Flint as a long-lived subprocess driven over stream-json,
2
+ // the way a host drives an agent CLI.
3
+ //
4
+ // One process is one session. Each `user` line on stdin is one turn; turns
5
+ // run one at a time, in order. While a turn runs, its model replies and tool
6
+ // results are written to stdout as they happen, and the turn ends with one
7
+ // `result` line. A `control_request` with subtype "interrupt" stops the
8
+ // running turn, which then ends with a `result` of subtype
9
+ // "error_during_execution". The process stays up
10
+ // between turns and exits when stdin closes, after the running turn.
11
+ //
12
+ // No terminal UI, no HTTP server, no message bus: a host that starts several
13
+ // of these gets several independent agents, not several readers of one queue.
14
+
15
+ import { createInterface } from "node:readline";
16
+ import { store } from "../store/index.js";
17
+ import { app } from "../app-state.js";
18
+ import { config } from "../config.js";
19
+ import { processMessage, setTurnObserver } from "../message-handler.js";
20
+ import { bulkSetPermission, setUnattended } from "../tools/permissions.js";
21
+ import { getDefinitions } from "../tools/registry.js";
22
+ import { killAllChildrenSync } from "../tools/process-tools.js";
23
+ import { saveSession } from "../sessions.js";
24
+ import { sessionData } from "../app-state.js";
25
+ import { initEvent } from "./protocol.js";
26
+ import { createStdioSession } from "./session.js";
27
+
28
+ const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07/g;
29
+
30
+ /** Run the stdio mode until stdin closes. Never returns. */
31
+ export async function runStdio({ opts, write, version }) {
32
+ config.headless = true;
33
+ if (opts.model) {
34
+ config.model = opts.model;
35
+ store.getState().setModel(opts.model);
36
+ }
37
+ if (opts.skipPermissions) bulkSetPermission("allow");
38
+
39
+ // Every store line goes to stderr: the host keeps it for diagnosis
40
+ // (gateway: the stderr tail in the session's error record).
41
+ const addLine = store.getState().addLine.bind(store.getState());
42
+ store.setState({
43
+ addLine: (text, ...rest) => {
44
+ const raw = typeof text === "string" ? text.replace(ANSI_RE, "") : String(text ?? "");
45
+ if (raw.trim()) process.stderr.write(raw.replace(/\s+$/, "") + "\n");
46
+ return addLine(text, ...rest);
47
+ },
48
+ });
49
+
50
+ // The MCP servers connect in the background; the first turn waits for
51
+ // them (up to 15 s, as the bus does for API messages), or the agent would
52
+ // start without the tools its host gave it.
53
+ const waitStart = Date.now();
54
+ while (!app.mcpReady && Date.now() - waitStart < 15000) await new Promise((r) => setTimeout(r, 200));
55
+
56
+ const sessionId = store.getState().sessionId;
57
+ write(initEvent({
58
+ sessionId,
59
+ model: config.model,
60
+ cwd: process.cwd(),
61
+ tools: getDefinitions().map((t) => t.function?.name).filter(Boolean),
62
+ mcpServers: (app.mcpStatusList || []).map((s) => ({ name: s.name, status: s.ok ? "connected" : "failed" })),
63
+ permissionMode: opts.skipPermissions ? "bypassPermissions" : "default",
64
+ version,
65
+ }));
66
+
67
+ const session = createStdioSession({
68
+ write,
69
+ sessionId,
70
+ model: () => config.model,
71
+ run: async (content, { signal, observer }) => {
72
+ // Nobody can answer an approval prompt here: a tool that would ask is
73
+ // refused at once (permissions.js, unattended), unless the host passed
74
+ // --dangerously-skip-permissions.
75
+ setUnattended(true);
76
+ setTurnObserver(observer);
77
+ try {
78
+ return await processMessage(content, null, { signal });
79
+ } finally {
80
+ setTurnObserver(null);
81
+ }
82
+ },
83
+ // What an agent CLI leaves in the transcript after a stop. Without it the
84
+ // next turn read the stopped command as unfinished work and ran it again,
85
+ // in the background this time (2026-10-02).
86
+ onInterrupted: async () => {
87
+ store.getState().pushMessage({ role: "user", content: "[Request interrupted by user. Do not resume that work unless asked again.]" });
88
+ await saveSession(sessionId, sessionData(store));
89
+ },
90
+ onIdleEnd: () => shutdown(0),
91
+ });
92
+
93
+ // Background processes the agent started end with it: a host that closes
94
+ // stdin or kills this process expects nothing of it left behind.
95
+ function shutdown(code) {
96
+ try { killAllChildrenSync(); } catch {}
97
+ process.exit(code);
98
+ }
99
+ process.on("SIGTERM", () => shutdown(143));
100
+ process.on("SIGINT", () => shutdown(130));
101
+
102
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
103
+ rl.on("line", (l) => session.line(l));
104
+ rl.on("close", () => session.end());
105
+ await new Promise(() => {});
106
+ }
@@ -0,0 +1,180 @@
1
+ // The stdio mode's session logic, kept free of the agent's own modules so it
2
+ // can be tested alone: the host's instructions (CLAUDE.md and the prompt
3
+ // flags), the MCP config file, and the loop that turns input lines into turns
4
+ // and turns into protocol lines. stdio/run.js wires it to the agent.
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import {
9
+ parseInputLine, userContent, assistantEvent, toolResultEvent,
10
+ resultEvent, resultSubtype, controlResponse,
11
+ } from "./protocol.js";
12
+
13
+ /**
14
+ * CLAUDE.md files an agent in this folder is expected to read: the folder's
15
+ * own and every parent's, outermost first. A host may keep an agent's role
16
+ * there and shared rules one level up.
17
+ */
18
+ export function claudeMdChain(cwd) {
19
+ const found = [];
20
+ let dir = path.resolve(cwd);
21
+ for (;;) {
22
+ const f = path.join(dir, "CLAUDE.md");
23
+ if (existsSync(f)) found.unshift(f);
24
+ const up = path.dirname(dir);
25
+ if (up === dir) break;
26
+ dir = up;
27
+ }
28
+ return found;
29
+ }
30
+
31
+ /**
32
+ * The host's instructions, in the order they apply: --system-prompt (or its
33
+ * file), the CLAUDE.md chain, then --append-system-prompt (or its file).
34
+ */
35
+ export function hostPromptFrom(opts, { cwd = process.cwd(), read = (f) => readFileSync(f, "utf8") } = {}) {
36
+ const parts = [];
37
+ if (opts.systemPrompt) parts.push(opts.systemPrompt);
38
+ if (opts.systemPromptFile) parts.push(read(opts.systemPromptFile));
39
+ for (const f of claudeMdChain(cwd)) {
40
+ const text = read(f).trim();
41
+ if (text) parts.push(`<instructions source="${f}">\n${text}\n</instructions>`);
42
+ }
43
+ if (opts.appendSystemPrompt) parts.push(opts.appendSystemPrompt);
44
+ if (opts.appendSystemPromptFile) parts.push(read(opts.appendSystemPromptFile));
45
+ const body = parts.map((p) => String(p).trim()).filter(Boolean).join("\n\n");
46
+ if (!body) return "";
47
+ // Without this line the model kept the core prompt's "You are FLINT" over
48
+ // the host's role: a test agent told to answer "Pebble" answered "FLINT."
49
+ // (2026-10-02). An agent's CLAUDE.md is who it is.
50
+ return "The program running you gave you the identity, role and rules below. " +
51
+ "They take precedence over the defaults above: where they name you, give you a role, " +
52
+ "or say how to answer, follow them.\n\n" + body;
53
+ }
54
+
55
+ /** The MCP config file to load: --mcp-config, else .mcp.json in the folder. */
56
+ export function mcpConfigPath(opts, cwd = process.cwd()) {
57
+ if (opts.mcpConfig) return path.resolve(cwd, opts.mcpConfig);
58
+ const local = path.join(cwd, ".mcp.json");
59
+ return existsSync(local) ? local : null;
60
+ }
61
+
62
+ /**
63
+ * Turns one session's lines into turns. `write` takes one protocol object;
64
+ * `run` runs one turn (processMessage); both are injectable for tests.
65
+ */
66
+ export function createStdioSession({ write, run, sessionId, model, onIdleEnd, onInterrupted }) {
67
+ const modelName = typeof model === "function" ? model : () => model;
68
+ const queue = [];
69
+ let current = null; // { controller, interrupted }
70
+ let busy = false;
71
+ let ended = false;
72
+ let turnNo = 0;
73
+
74
+ async function runTurn(content) {
75
+ turnNo++;
76
+ const controller = new AbortController();
77
+ current = { controller, interrupted: false };
78
+ const started = Date.now();
79
+ let apiCalls = 0;
80
+ let toolSeq = 0;
81
+ const pendingIds = new Map(); // tool name -> ids of its calls not yet answered
82
+
83
+ const observer = {
84
+ onReply(reply, usage) {
85
+ apiCalls++;
86
+ for (const tc of reply?.tool_calls || []) {
87
+ if (!tc.id) tc.id = `toolu_flint_${turnNo}_${++toolSeq}`;
88
+ const name = tc.function?.name;
89
+ if (!pendingIds.has(name)) pendingIds.set(name, []);
90
+ pendingIds.get(name).push(tc.id);
91
+ }
92
+ const ev = assistantEvent({ reply, usage, model: modelName(), sessionId, messageId: `msg_flint_${turnNo}_${apiCalls}` });
93
+ if (ev) write(ev);
94
+ },
95
+ onToolResult(name, result, denied) {
96
+ const id = pendingIds.get(name)?.shift() || `toolu_flint_${turnNo}_${++toolSeq}`;
97
+ const text = typeof result === "string" ? result : JSON.stringify(result);
98
+ const isError = !!denied || /^(error|\[error)/i.test(String(text || "").trim());
99
+ write(toolResultEvent({ toolUseId: id, result: text, isError, sessionId }));
100
+ },
101
+ };
102
+
103
+ let ev;
104
+ try {
105
+ const res = await run(content, { signal: controller.signal, observer });
106
+ const stats = res?.stats || {};
107
+ const subtype = current.interrupted ? "error_during_execution" : resultSubtype(res?.stop_reason);
108
+ ev = resultEvent({
109
+ subtype,
110
+ text: res?.text || "",
111
+ sessionId,
112
+ durationMs: Date.now() - started,
113
+ numTurns: apiCalls,
114
+ costUsd: stats.cost,
115
+ promptTokens: stats.promptTokens,
116
+ completionTokens: stats.completionTokens,
117
+ stopReason: current.interrupted ? "interrupted" : (res?.stop_reason || "done"),
118
+ });
119
+ } catch (err) {
120
+ const interrupted = current.interrupted || err?.name === "AbortError";
121
+ ev = resultEvent({
122
+ subtype: "error_during_execution",
123
+ text: interrupted ? "" : `Error: ${err?.message || err}`,
124
+ sessionId,
125
+ durationMs: Date.now() - started,
126
+ numTurns: apiCalls,
127
+ stopReason: interrupted ? "interrupted" : "error",
128
+ });
129
+ } finally {
130
+ if (current?.interrupted) {
131
+ try { await onInterrupted?.(); } catch {}
132
+ }
133
+ current = null;
134
+ }
135
+ write(ev);
136
+ }
137
+
138
+ async function pump() {
139
+ if (busy) return;
140
+ busy = true;
141
+ try {
142
+ while (queue.length) await runTurn(queue.shift());
143
+ } finally {
144
+ busy = false;
145
+ }
146
+ if (ended) onIdleEnd?.();
147
+ }
148
+
149
+ return {
150
+ /** One input line. */
151
+ line(raw) {
152
+ const msg = parseInputLine(raw);
153
+ if (!msg) return;
154
+ if (msg.type === "user") {
155
+ const content = userContent(msg);
156
+ if (content == null) return;
157
+ queue.push(content);
158
+ pump();
159
+ return;
160
+ }
161
+ if (msg.type === "control_request") {
162
+ const subtype = msg.request?.subtype;
163
+ if (subtype === "interrupt" && current) {
164
+ current.interrupted = true;
165
+ current.controller.abort(new Error("interrupted by host"));
166
+ }
167
+ // Every request is answered, so a host waiting on one never hangs:
168
+ // interrupt with nothing running, initialize, and the requests Flint
169
+ // has no use for all get a plain success.
170
+ write(controlResponse(msg.request_id ?? null));
171
+ }
172
+ },
173
+ /** stdin closed: finish what is queued, then end. */
174
+ end() {
175
+ ended = true;
176
+ if (!busy) onIdleEnd?.();
177
+ },
178
+ get busy() { return busy; },
179
+ };
180
+ }