@ouro.bot/cli 0.0.1-alpha.0 → 0.1.0-alpha.2

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 (119) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +20 -0
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
  3. package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
  4. package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
  5. package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
  6. package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
  7. package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
  8. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
  9. package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
  10. package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
  11. package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
  12. package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
  13. package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
  14. package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
  15. package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
  16. package/README.md +224 -6
  17. package/dist/heart/agent-entry.js +17 -0
  18. package/dist/heart/api-error.js +34 -0
  19. package/dist/heart/config.js +296 -0
  20. package/dist/heart/core.js +515 -0
  21. package/dist/heart/daemon/daemon-cli.js +675 -0
  22. package/dist/heart/daemon/daemon-entry.js +74 -0
  23. package/dist/heart/daemon/daemon.js +313 -0
  24. package/dist/heart/daemon/hatch-flow.js +285 -0
  25. package/dist/heart/daemon/hatch-specialist.js +107 -0
  26. package/dist/heart/daemon/health-monitor.js +79 -0
  27. package/dist/heart/daemon/log-tailer.js +146 -0
  28. package/dist/heart/daemon/message-router.js +98 -0
  29. package/dist/heart/daemon/os-cron.js +260 -0
  30. package/dist/heart/daemon/ouro-bot-entry.js +23 -0
  31. package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
  32. package/dist/heart/daemon/ouro-entry.js +23 -0
  33. package/dist/heart/daemon/ouro-uti.js +212 -0
  34. package/dist/heart/daemon/process-manager.js +237 -0
  35. package/dist/heart/daemon/runtime-logging.js +98 -0
  36. package/dist/heart/daemon/subagent-installer.js +125 -0
  37. package/dist/heart/daemon/task-scheduler.js +240 -0
  38. package/dist/heart/harness.js +26 -0
  39. package/dist/heart/identity.js +281 -0
  40. package/dist/heart/kicks.js +144 -0
  41. package/dist/heart/primitives.js +4 -0
  42. package/dist/heart/providers/anthropic.js +329 -0
  43. package/dist/heart/providers/azure.js +66 -0
  44. package/dist/heart/providers/minimax.js +53 -0
  45. package/dist/heart/providers/openai-codex.js +162 -0
  46. package/dist/heart/streaming.js +412 -0
  47. package/dist/heart/turn-coordinator.js +62 -0
  48. package/dist/inner-worker-entry.js +4 -0
  49. package/dist/mind/associative-recall.js +197 -0
  50. package/dist/mind/bundle-manifest.js +118 -0
  51. package/dist/mind/context.js +302 -0
  52. package/dist/mind/first-impressions.js +43 -0
  53. package/dist/mind/format.js +56 -0
  54. package/dist/mind/friends/channel.js +41 -0
  55. package/dist/mind/friends/resolver.js +84 -0
  56. package/dist/mind/friends/store-file.js +171 -0
  57. package/dist/mind/friends/store.js +4 -0
  58. package/dist/mind/friends/tokens.js +26 -0
  59. package/dist/mind/friends/types.js +21 -0
  60. package/dist/mind/memory.js +388 -0
  61. package/dist/mind/pending.js +93 -0
  62. package/dist/mind/phrases.js +43 -0
  63. package/dist/mind/prompt-refresh.js +20 -0
  64. package/dist/mind/prompt.js +352 -0
  65. package/dist/mind/token-estimate.js +119 -0
  66. package/dist/nerves/cli-logging.js +31 -0
  67. package/dist/nerves/coverage/audit-rules.js +81 -0
  68. package/dist/nerves/coverage/audit.js +200 -0
  69. package/dist/nerves/coverage/cli-main.js +5 -0
  70. package/dist/nerves/coverage/cli.js +51 -0
  71. package/dist/nerves/coverage/contract.js +23 -0
  72. package/dist/nerves/coverage/file-completeness.js +56 -0
  73. package/dist/nerves/coverage/run-artifacts.js +77 -0
  74. package/dist/nerves/coverage/source-scanner.js +34 -0
  75. package/dist/nerves/index.js +152 -0
  76. package/dist/nerves/runtime.js +38 -0
  77. package/dist/repertoire/ado-client.js +211 -0
  78. package/dist/repertoire/ado-context.js +73 -0
  79. package/dist/repertoire/ado-semantic.js +841 -0
  80. package/dist/repertoire/ado-templates.js +146 -0
  81. package/dist/repertoire/coding/index.js +36 -0
  82. package/dist/repertoire/coding/manager.js +489 -0
  83. package/dist/repertoire/coding/monitor.js +60 -0
  84. package/dist/repertoire/coding/reporter.js +45 -0
  85. package/dist/repertoire/coding/spawner.js +102 -0
  86. package/dist/repertoire/coding/tools.js +167 -0
  87. package/dist/repertoire/coding/types.js +2 -0
  88. package/dist/repertoire/data/ado-endpoints.json +122 -0
  89. package/dist/repertoire/data/graph-endpoints.json +212 -0
  90. package/dist/repertoire/github-client.js +64 -0
  91. package/dist/repertoire/graph-client.js +118 -0
  92. package/dist/repertoire/skills.js +156 -0
  93. package/dist/repertoire/tasks/board.js +122 -0
  94. package/dist/repertoire/tasks/index.js +210 -0
  95. package/dist/repertoire/tasks/lifecycle.js +80 -0
  96. package/dist/repertoire/tasks/middleware.js +65 -0
  97. package/dist/repertoire/tasks/parser.js +173 -0
  98. package/dist/repertoire/tasks/scanner.js +132 -0
  99. package/dist/repertoire/tasks/transitions.js +145 -0
  100. package/dist/repertoire/tasks/types.js +2 -0
  101. package/dist/repertoire/tools-base.js +714 -0
  102. package/dist/repertoire/tools-github.js +53 -0
  103. package/dist/repertoire/tools-teams.js +308 -0
  104. package/dist/repertoire/tools.js +199 -0
  105. package/dist/senses/cli-entry.js +15 -0
  106. package/dist/senses/cli.js +604 -0
  107. package/dist/senses/commands.js +98 -0
  108. package/dist/senses/inner-dialog-worker.js +61 -0
  109. package/dist/senses/inner-dialog.js +231 -0
  110. package/dist/senses/session-lock.js +119 -0
  111. package/dist/senses/teams-entry.js +15 -0
  112. package/dist/senses/teams.js +696 -0
  113. package/dist/senses/trust-gate.js +150 -0
  114. package/package.json +34 -11
  115. package/subagents/README.md +73 -0
  116. package/subagents/work-doer.md +233 -0
  117. package/subagents/work-merger.md +624 -0
  118. package/subagents/work-planner.md +373 -0
  119. package/bin/ouro.js +0 -6
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createOpenAICodexProviderRuntime = createOpenAICodexProviderRuntime;
7
+ const openai_1 = __importDefault(require("openai"));
8
+ const config_1 = require("../config");
9
+ const identity_1 = require("../identity");
10
+ const runtime_1 = require("../../nerves/runtime");
11
+ const streaming_1 = require("../streaming");
12
+ const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
13
+ "authentication failed",
14
+ "unauthorized",
15
+ "invalid api key",
16
+ "invalid x-api-key",
17
+ "invalid bearer token",
18
+ ];
19
+ const OPENAI_CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
20
+ function getOpenAICodexSecretsPathForGuidance() {
21
+ return (0, identity_1.getAgentSecretsPath)();
22
+ }
23
+ function getOpenAICodexAgentNameForGuidance() {
24
+ return (0, identity_1.getAgentName)();
25
+ }
26
+ function getOpenAICodexOAuthInstructions() {
27
+ const agentName = getOpenAICodexAgentNameForGuidance();
28
+ return [
29
+ "Fix:",
30
+ ` 1. Run \`npm run auth:openai-codex -- --agent ${agentName}\``,
31
+ " (or run `codex login` and set the OAuth token manually)",
32
+ ` 2. Open ${getOpenAICodexSecretsPathForGuidance()}`,
33
+ " 3. Confirm providers.openai-codex.oauthAccessToken is set",
34
+ " 4. This provider uses chatgpt.com/backend-api/codex/responses (not api.openai.com/responses).",
35
+ ].join("\n");
36
+ }
37
+ function getOpenAICodexReauthGuidance(reason) {
38
+ return [
39
+ "OpenAI Codex configuration error.",
40
+ reason,
41
+ getOpenAICodexOAuthInstructions(),
42
+ ].join("\n");
43
+ }
44
+ function isOpenAICodexAuthFailure(error) {
45
+ if (!(error instanceof Error))
46
+ return false;
47
+ const status = error.status;
48
+ if (status === 401 || status === 403)
49
+ return true;
50
+ const lower = error.message.toLowerCase();
51
+ return OPENAI_CODEX_AUTH_FAILURE_MARKERS.some((marker) => lower.includes(marker));
52
+ }
53
+ function withOpenAICodexAuthGuidance(error) {
54
+ const base = error instanceof Error ? error.message : String(error);
55
+ if (isOpenAICodexAuthFailure(error)) {
56
+ return new Error(getOpenAICodexReauthGuidance(`OpenAI Codex authentication failed (${base}).`));
57
+ }
58
+ return error instanceof Error ? error : new Error(String(error));
59
+ }
60
+ function decodeJwtPayload(token) {
61
+ const parts = token.split(".");
62
+ if (parts.length < 2)
63
+ return null;
64
+ try {
65
+ const padded = parts[1]
66
+ .replace(/-/g, "+")
67
+ .replace(/_/g, "/")
68
+ .padEnd(Math.ceil(parts[1].length / 4) * 4, "=");
69
+ const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
70
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
71
+ return null;
72
+ return payload;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ function getChatGPTAccountIdFromToken(token) {
79
+ const payload = decodeJwtPayload(token);
80
+ if (!payload)
81
+ return "";
82
+ const authClaim = payload["https://api.openai.com/auth"];
83
+ if (!authClaim || typeof authClaim !== "object" || Array.isArray(authClaim))
84
+ return "";
85
+ const accountId = authClaim.chatgpt_account_id;
86
+ if (typeof accountId !== "string")
87
+ return "";
88
+ return accountId.trim();
89
+ }
90
+ function createOpenAICodexProviderRuntime() {
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "engine",
93
+ event: "engine.provider_init",
94
+ message: "openai-codex provider init",
95
+ meta: { provider: "openai-codex" },
96
+ });
97
+ const codexConfig = (0, config_1.getOpenAICodexConfig)();
98
+ if (!(codexConfig.model && codexConfig.oauthAccessToken)) {
99
+ throw new Error(getOpenAICodexReauthGuidance("provider 'openai-codex' is selected in agent.json but providers.openai-codex.model/oauthAccessToken is incomplete in secrets.json."));
100
+ }
101
+ const token = codexConfig.oauthAccessToken.trim();
102
+ if (!token) {
103
+ throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is empty."));
104
+ }
105
+ const chatgptAccountId = getChatGPTAccountIdFromToken(token);
106
+ if (!chatgptAccountId) {
107
+ throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is missing a chatgpt_account_id claim required for chatgpt.com/backend-api/codex."));
108
+ }
109
+ const client = new openai_1.default({
110
+ apiKey: token,
111
+ baseURL: OPENAI_CODEX_BACKEND_BASE_URL,
112
+ defaultHeaders: {
113
+ "chatgpt-account-id": chatgptAccountId,
114
+ "OpenAI-Beta": "responses=experimental",
115
+ originator: "ouroboros",
116
+ },
117
+ timeout: 30000,
118
+ maxRetries: 0,
119
+ });
120
+ let nativeInput = null;
121
+ let nativeInstructions = "";
122
+ return {
123
+ id: "openai-codex",
124
+ model: codexConfig.model,
125
+ client,
126
+ resetTurnState(messages) {
127
+ const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
128
+ nativeInput = input;
129
+ nativeInstructions = instructions;
130
+ },
131
+ appendToolOutput(callId, output) {
132
+ if (!nativeInput)
133
+ return;
134
+ nativeInput.push({ type: "function_call_output", call_id: callId, output });
135
+ },
136
+ async streamTurn(request) {
137
+ if (!nativeInput)
138
+ this.resetTurnState(request.messages);
139
+ const params = {
140
+ model: this.model,
141
+ input: nativeInput,
142
+ instructions: nativeInstructions,
143
+ tools: (0, streaming_1.toResponsesTools)(request.activeTools),
144
+ reasoning: { effort: "medium", summary: "detailed" },
145
+ stream: true,
146
+ store: false,
147
+ include: ["reasoning.encrypted_content"],
148
+ };
149
+ if (request.toolChoiceRequired)
150
+ params.tool_choice = "required";
151
+ try {
152
+ const result = await (0, streaming_1.streamResponsesApi)(this.client, params, request.callbacks, request.signal);
153
+ for (const item of result.outputItems)
154
+ nativeInput.push(item);
155
+ return result;
156
+ }
157
+ catch (error) {
158
+ throw withOpenAICodexAuthGuidance(error);
159
+ }
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FinalAnswerParser = void 0;
4
+ exports.toResponsesInput = toResponsesInput;
5
+ exports.toResponsesTools = toResponsesTools;
6
+ exports.streamChatCompletion = streamChatCompletion;
7
+ exports.streamResponsesApi = streamResponsesApi;
8
+ const runtime_1 = require("../nerves/runtime");
9
+ // Character-level state machine that extracts the answer value from
10
+ // `final_answer` tool call JSON arguments as they stream in.
11
+ // Scans for prefix `"answer":"` or `"answer": "` in the character stream,
12
+ // then emits text handling JSON escapes, stopping at unescaped closing `"`.
13
+ class FinalAnswerParser {
14
+ // Possible prefixes to match (with and without space after colon)
15
+ static PREFIXES = ['"answer":"', '"answer": "'];
16
+ // Buffer of characters seen so far (pre-activation only)
17
+ buf = "";
18
+ _active = false;
19
+ _complete = false;
20
+ inEscape = false;
21
+ get active() { return this._active; }
22
+ get complete() { return this._complete; }
23
+ process(delta) {
24
+ if (this._complete)
25
+ return "";
26
+ let out = "";
27
+ for (let i = 0; i < delta.length; i++) {
28
+ const ch = delta[i];
29
+ if (!this._active) {
30
+ this.buf += ch;
31
+ // Check if any prefix has been fully matched in the buffer
32
+ for (const prefix of FinalAnswerParser.PREFIXES) {
33
+ if (this.buf.endsWith(prefix)) {
34
+ this._active = true;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ else {
40
+ // Active: emit characters, handling JSON escapes
41
+ if (this.inEscape) {
42
+ this.inEscape = false;
43
+ switch (ch) {
44
+ case '"':
45
+ out += '"';
46
+ break;
47
+ case '\\':
48
+ out += '\\';
49
+ break;
50
+ case 'n':
51
+ out += '\n';
52
+ break;
53
+ case 't':
54
+ out += '\t';
55
+ break;
56
+ case '/':
57
+ out += '/';
58
+ break;
59
+ default:
60
+ out += ch;
61
+ break; // unknown escape: pass through character
62
+ }
63
+ }
64
+ else if (ch === '\\') {
65
+ this.inEscape = true;
66
+ }
67
+ else if (ch === '"') {
68
+ this._complete = true;
69
+ return out; // stop processing, closing quote found
70
+ }
71
+ else {
72
+ out += ch;
73
+ }
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ }
79
+ exports.FinalAnswerParser = FinalAnswerParser;
80
+ function toResponsesInput(messages) {
81
+ let instructions = "";
82
+ const input = [];
83
+ for (const msg of messages) {
84
+ if (msg.role === "system") {
85
+ if (!instructions) {
86
+ const sys = msg;
87
+ instructions = (typeof sys.content === "string" ? sys.content : "") || "";
88
+ }
89
+ continue;
90
+ }
91
+ if (msg.role === "user") {
92
+ const u = msg;
93
+ input.push({ role: "user", content: typeof u.content === "string" ? u.content : "" });
94
+ continue;
95
+ }
96
+ if (msg.role === "assistant") {
97
+ const a = msg;
98
+ // Restore reasoning items before content (matching API item order)
99
+ if (a._reasoning_items) {
100
+ for (const ri of a._reasoning_items) {
101
+ input.push(ri);
102
+ }
103
+ }
104
+ if (a.content) {
105
+ input.push({ role: "assistant", content: typeof a.content === "string" ? a.content : "" });
106
+ }
107
+ if (a.tool_calls) {
108
+ for (const tc of a.tool_calls) {
109
+ input.push({
110
+ type: "function_call",
111
+ call_id: tc.id,
112
+ name: tc.function.name,
113
+ arguments: tc.function.arguments,
114
+ status: "completed",
115
+ });
116
+ }
117
+ }
118
+ continue;
119
+ }
120
+ if (msg.role === "tool") {
121
+ const t = msg;
122
+ input.push({
123
+ type: "function_call_output",
124
+ call_id: t.tool_call_id,
125
+ output: typeof t.content === "string" ? t.content : "",
126
+ });
127
+ continue;
128
+ }
129
+ }
130
+ return { instructions, input };
131
+ }
132
+ function toResponsesTools(ccTools) {
133
+ return ccTools.map((t) => ({
134
+ type: "function",
135
+ name: t.function.name,
136
+ description: t.function.description ?? null,
137
+ parameters: t.function.parameters ?? null,
138
+ strict: false,
139
+ }));
140
+ }
141
+ async function streamChatCompletion(client, createParams, callbacks, signal) {
142
+ (0, runtime_1.emitNervesEvent)({
143
+ component: "engine",
144
+ event: "engine.stream_start",
145
+ message: "chat completion stream start",
146
+ meta: {},
147
+ });
148
+ // Request usage data in the final streaming chunk
149
+ createParams.stream_options = { include_usage: true };
150
+ const response = await client.chat.completions.create(createParams, signal ? { signal } : {});
151
+ let content = "";
152
+ let toolCalls = {};
153
+ let streamStarted = false;
154
+ let usage;
155
+ const answerParser = new FinalAnswerParser();
156
+ let finalAnswerDetected = false;
157
+ // State machine for parsing inline <think> tags (MiniMax pattern)
158
+ let contentBuf = "";
159
+ let inThinkTag = false;
160
+ const OPEN_TAG = "<think>";
161
+ const CLOSE_TAG = "</think>";
162
+ function processContentBuf(flush) {
163
+ while (contentBuf.length > 0) {
164
+ if (inThinkTag) {
165
+ const end = contentBuf.indexOf(CLOSE_TAG);
166
+ if (end !== -1) {
167
+ const reasoning = contentBuf.slice(0, end);
168
+ if (reasoning)
169
+ callbacks.onReasoningChunk(reasoning);
170
+ contentBuf = contentBuf.slice(end + CLOSE_TAG.length);
171
+ inThinkTag = false;
172
+ }
173
+ else {
174
+ // Check if buffer ends with a partial </think> prefix
175
+ if (!flush) {
176
+ let retain = 0;
177
+ for (let i = 1; i < CLOSE_TAG.length && i <= contentBuf.length; i++) {
178
+ if (contentBuf.endsWith(CLOSE_TAG.slice(0, i)))
179
+ retain = i;
180
+ }
181
+ if (retain > 0) {
182
+ const reasoning = contentBuf.slice(0, -retain);
183
+ if (reasoning)
184
+ callbacks.onReasoningChunk(reasoning);
185
+ contentBuf = contentBuf.slice(-retain);
186
+ return;
187
+ }
188
+ }
189
+ // All reasoning, flush it
190
+ callbacks.onReasoningChunk(contentBuf);
191
+ contentBuf = "";
192
+ }
193
+ }
194
+ else {
195
+ const start = contentBuf.indexOf(OPEN_TAG);
196
+ if (start !== -1) {
197
+ const text = contentBuf.slice(0, start);
198
+ if (text)
199
+ callbacks.onTextChunk(text);
200
+ contentBuf = contentBuf.slice(start + OPEN_TAG.length);
201
+ inThinkTag = true;
202
+ }
203
+ else {
204
+ // Check if buffer ends with a partial <think> prefix
205
+ if (!flush) {
206
+ let retain = 0;
207
+ for (let i = 1; i < OPEN_TAG.length && i <= contentBuf.length; i++) {
208
+ if (contentBuf.endsWith(OPEN_TAG.slice(0, i)))
209
+ retain = i;
210
+ }
211
+ if (retain > 0) {
212
+ const text = contentBuf.slice(0, -retain);
213
+ if (text)
214
+ callbacks.onTextChunk(text);
215
+ contentBuf = contentBuf.slice(-retain);
216
+ return;
217
+ }
218
+ }
219
+ // All content, flush it
220
+ callbacks.onTextChunk(contentBuf);
221
+ contentBuf = "";
222
+ }
223
+ }
224
+ }
225
+ }
226
+ for await (const chunk of response) {
227
+ if (signal?.aborted)
228
+ break;
229
+ // Capture usage from final chunk (sent when stream_options.include_usage is true)
230
+ if (chunk.usage) {
231
+ const u = chunk.usage;
232
+ usage = {
233
+ input_tokens: u.prompt_tokens,
234
+ output_tokens: u.completion_tokens,
235
+ reasoning_tokens: u.completion_tokens_details?.reasoning_tokens ?? 0,
236
+ total_tokens: u.total_tokens,
237
+ };
238
+ }
239
+ const d = chunk.choices[0]?.delta;
240
+ if (!d)
241
+ continue;
242
+ // Handle reasoning_content (Azure AI models like DeepSeek-R1)
243
+ if (d.reasoning_content) {
244
+ if (!streamStarted) {
245
+ callbacks.onModelStreamStart();
246
+ streamStarted = true;
247
+ }
248
+ callbacks.onReasoningChunk(d.reasoning_content);
249
+ }
250
+ if (d.content) {
251
+ if (!streamStarted) {
252
+ callbacks.onModelStreamStart();
253
+ streamStarted = true;
254
+ }
255
+ content += d.content;
256
+ contentBuf += d.content;
257
+ processContentBuf(false);
258
+ }
259
+ if (d.tool_calls) {
260
+ for (const tc of d.tool_calls) {
261
+ if (!toolCalls[tc.index])
262
+ toolCalls[tc.index] = {
263
+ id: tc.id ?? "",
264
+ name: tc.function?.name ?? "",
265
+ arguments: "",
266
+ };
267
+ if (tc.id)
268
+ toolCalls[tc.index].id = tc.id;
269
+ if (tc.function?.name) {
270
+ toolCalls[tc.index].name = tc.function.name;
271
+ // Detect final_answer tool call on first name delta.
272
+ // Only activate streaming if this is the sole tool call (index 0
273
+ // and no other indices seen). Mixed calls are rejected by core.ts.
274
+ if (tc.function.name === "final_answer" && !finalAnswerDetected
275
+ && tc.index === 0 && Object.keys(toolCalls).length === 1) {
276
+ finalAnswerDetected = true;
277
+ callbacks.onClearText?.();
278
+ }
279
+ }
280
+ if (tc.function?.arguments) {
281
+ toolCalls[tc.index].arguments += tc.function.arguments;
282
+ // Feed final_answer argument deltas to the parser for progressive
283
+ // streaming, but only when it appears to be the sole tool call.
284
+ if (finalAnswerDetected && toolCalls[tc.index].name === "final_answer"
285
+ && Object.keys(toolCalls).length === 1) {
286
+ const text = answerParser.process(tc.function.arguments);
287
+ if (text)
288
+ callbacks.onTextChunk(text);
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+ // Flush any remaining buffer at end of stream
295
+ if (contentBuf)
296
+ processContentBuf(true);
297
+ return {
298
+ content,
299
+ toolCalls: Object.values(toolCalls),
300
+ outputItems: [],
301
+ usage,
302
+ finalAnswerStreamed: answerParser.active,
303
+ };
304
+ }
305
+ async function streamResponsesApi(client, createParams, callbacks, signal) {
306
+ (0, runtime_1.emitNervesEvent)({
307
+ component: "engine",
308
+ event: "engine.stream_start",
309
+ message: "responses API stream start",
310
+ meta: {},
311
+ });
312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Azure Responses API not in OpenAI SDK types
313
+ const response = await client.responses.create(createParams, signal ? { signal } : {});
314
+ let content = "";
315
+ let streamStarted = false;
316
+ const toolCalls = [];
317
+ const outputItems = [];
318
+ let currentToolCall = null;
319
+ let usage;
320
+ const answerParser = new FinalAnswerParser();
321
+ let functionCallCount = 0;
322
+ let finalAnswerDetected = false;
323
+ for await (const event of response) {
324
+ if (signal?.aborted)
325
+ break;
326
+ switch (event.type) {
327
+ case "response.output_text.delta":
328
+ case "response.reasoning_summary_text.delta": {
329
+ if (!streamStarted) {
330
+ callbacks.onModelStreamStart();
331
+ streamStarted = true;
332
+ }
333
+ const delta = String(event.delta);
334
+ if (event.type === "response.output_text.delta") {
335
+ callbacks.onTextChunk(delta);
336
+ content += delta;
337
+ }
338
+ else {
339
+ callbacks.onReasoningChunk(delta);
340
+ }
341
+ break;
342
+ }
343
+ case "response.output_item.added": {
344
+ if (event.item?.type === "function_call") {
345
+ functionCallCount++;
346
+ currentToolCall = {
347
+ call_id: String(event.item.call_id),
348
+ name: String(event.item.name),
349
+ arguments: "",
350
+ };
351
+ // Detect final_answer function call -- clear any streamed noise.
352
+ // Only activate when this is the first (and so far only) function call.
353
+ // Mixed calls are rejected by core.ts; no need to stream their args.
354
+ if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
355
+ finalAnswerDetected = true;
356
+ callbacks.onClearText?.();
357
+ }
358
+ }
359
+ break;
360
+ }
361
+ case "response.function_call_arguments.delta": {
362
+ if (currentToolCall) {
363
+ currentToolCall.arguments += event.delta;
364
+ // Feed final_answer argument deltas to the parser for progressive
365
+ // streaming, but only when it appears to be the sole function call.
366
+ if (finalAnswerDetected && currentToolCall.name === "final_answer"
367
+ && functionCallCount === 1) {
368
+ const text = answerParser.process(String(event.delta));
369
+ if (text)
370
+ callbacks.onTextChunk(text);
371
+ }
372
+ }
373
+ break;
374
+ }
375
+ case "response.output_item.done": {
376
+ outputItems.push(event.item);
377
+ if (event.item?.type === "function_call") {
378
+ toolCalls.push({
379
+ id: String(event.item.call_id),
380
+ name: String(event.item.name),
381
+ arguments: String(event.item.arguments),
382
+ });
383
+ currentToolCall = null;
384
+ }
385
+ break;
386
+ }
387
+ case "response.completed":
388
+ case "response.done": {
389
+ const u = event.response?.usage;
390
+ if (u) {
391
+ usage = {
392
+ input_tokens: u.input_tokens,
393
+ output_tokens: u.output_tokens,
394
+ reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? 0,
395
+ total_tokens: u.total_tokens,
396
+ };
397
+ }
398
+ break;
399
+ }
400
+ default:
401
+ // Unknown/unhandled events silently ignored
402
+ break;
403
+ }
404
+ }
405
+ return {
406
+ content,
407
+ toolCalls,
408
+ outputItems,
409
+ usage,
410
+ finalAnswerStreamed: answerParser.active,
411
+ };
412
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTurnCoordinator = createTurnCoordinator;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ function createTurnCoordinator() {
6
+ const turnLocks = new Map();
7
+ const activeTurns = new Set();
8
+ const followUpBuffers = new Map();
9
+ return {
10
+ async withTurnLock(key, fn) {
11
+ (0, runtime_1.emitNervesEvent)({
12
+ component: "engine",
13
+ event: "engine.turn_start",
14
+ message: "turn lock acquired",
15
+ meta: { key },
16
+ });
17
+ const prev = turnLocks.get(key) ?? Promise.resolve();
18
+ const run = prev.then(async () => {
19
+ activeTurns.add(key);
20
+ try {
21
+ return await fn();
22
+ }
23
+ finally {
24
+ activeTurns.delete(key);
25
+ }
26
+ });
27
+ const settled = run.then(() => undefined, () => undefined);
28
+ turnLocks.set(key, settled);
29
+ try {
30
+ return await run;
31
+ }
32
+ finally {
33
+ if (turnLocks.get(key) === settled)
34
+ turnLocks.delete(key);
35
+ }
36
+ },
37
+ tryBeginTurn(key) {
38
+ if (activeTurns.has(key))
39
+ return false;
40
+ activeTurns.add(key);
41
+ return true;
42
+ },
43
+ endTurn(key) {
44
+ activeTurns.delete(key);
45
+ },
46
+ isTurnActive(key) {
47
+ return activeTurns.has(key);
48
+ },
49
+ enqueueFollowUp(key, followUp) {
50
+ const current = followUpBuffers.get(key) ?? [];
51
+ current.push(followUp);
52
+ followUpBuffers.set(key, current);
53
+ },
54
+ drainFollowUps(key) {
55
+ const current = followUpBuffers.get(key);
56
+ if (!current || current.length === 0)
57
+ return [];
58
+ followUpBuffers.delete(key);
59
+ return [...current];
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Backward-compatible wrapper; unified runtime now lives at heart/agent-entry.
4
+ require("./heart/agent-entry");