@sema-agent/core 5.8.0 → 5.10.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 +82 -0
- package/dist/agents/cascade.js +24 -0
- package/dist/agents/roster-store.d.ts +1 -0
- package/dist/agents/send-message-tool.js +6 -0
- package/dist/agents/subagent.d.ts +33 -0
- package/dist/agents/subagent.js +125 -33
- package/dist/agents/teacher.js +15 -3
- package/dist/agents/team.js +10 -0
- package/dist/agents/verify.js +7 -0
- package/dist/brain/anthropic.js +27 -10
- package/dist/brain/open-responses.d.ts +11 -0
- package/dist/brain/open-responses.js +736 -0
- package/dist/brain/openai.js +32 -5
- package/dist/brain/request-params.d.ts +1 -0
- package/dist/brain/request-params.js +16 -0
- package/dist/core/a2a.js +1 -1
- package/dist/core/fs-write-gate-policy.js +2 -2
- package/dist/core/lsp-diagnostics.d.ts +3 -2
- package/dist/core/lsp-diagnostics.js +20 -7
- package/dist/core/memory-recall.js +8 -3
- package/dist/core/memory.d.ts +5 -0
- package/dist/core/memory.js +6 -4
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +14 -7
- package/dist/core/runner/prepare-task.d.ts +9 -1
- package/dist/core/runner/prepare-task.js +51 -14
- package/dist/core/runner/runtask.d.ts +12 -0
- package/dist/core/runner/runtask.js +153 -42
- package/dist/core/runner/session-file-state-replay.d.ts +7 -0
- package/dist/core/runner/session-file-state-replay.js +56 -0
- package/dist/core/runner/session-rule-policy.d.ts +1 -0
- package/dist/core/runner/session-rule-policy.js +4 -3
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/runner/tool-output-projection.js +5 -4
- package/dist/core/session-reconcile.d.ts +7 -3
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/strategy-store.d.ts +1 -1
- package/dist/core/strategy-store.js +27 -4
- package/dist/core/task-registry-shared.d.ts +0 -1
- package/dist/core/tool-policy.d.ts +8 -0
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/tools.js +9 -1
- package/dist/core/trace.d.ts +0 -2
- package/dist/core/types.d.ts +8 -1
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +1 -0
- package/dist/engine/llm/types.d.ts +2 -73
- package/dist/engine/loop/agent-loop.js +168 -22
- package/dist/engine/loop/types.d.ts +1 -0
- package/dist/engine/session/repo-utils.d.ts +1 -2
- package/dist/engine/session/repo-utils.js +0 -7
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.js +19 -0
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +4 -1
- package/dist/orchestration/workflow.js +15 -6
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +1 -1
- package/dist/stores/file/memory-store.js +3 -7
- package/dist/tools/fs/fs-bash.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +4 -0
- package/dist/tools/web.js +20 -20
- package/package.json +5 -3
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import {} from "../internal/llm.js";
|
|
2
|
+
import { BrainError } from "./errors.js";
|
|
3
|
+
import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
|
|
4
|
+
import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
|
|
5
|
+
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
|
+
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
|
+
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
|
+
import { RESPONSES_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
9
|
+
import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
|
|
10
|
+
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
|
+
const DEGENERATE_POLL_CHARS = 64;
|
|
12
|
+
const MALFORMED_SAMPLE_CHARS = 160;
|
|
13
|
+
const MALFORMED_ARGS_CHARS = 200;
|
|
14
|
+
const CAPABILITY_DIAGNOSTIC_TYPE = "responses_capability";
|
|
15
|
+
function responsesCompat(model) {
|
|
16
|
+
return (model.compat ?? {});
|
|
17
|
+
}
|
|
18
|
+
const NO_CONTENT = "(no content)";
|
|
19
|
+
const DOCUMENT_OMITTED = "[PDF document omitted: PDF direct read requires an Anthropic-family model; re-Read the file (the engine will extract text/render pages when possible) or extract the text via Bash instead (e.g. `pdftotext`).]";
|
|
20
|
+
function textOf(content) {
|
|
21
|
+
if (typeof content === "string")
|
|
22
|
+
return content;
|
|
23
|
+
return content
|
|
24
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
25
|
+
.map((c) => c.text)
|
|
26
|
+
.join("");
|
|
27
|
+
}
|
|
28
|
+
function functionCallOutputText(text, hasCarriedImages, original) {
|
|
29
|
+
if (text !== "")
|
|
30
|
+
return text;
|
|
31
|
+
if (hasCarriedImages)
|
|
32
|
+
return "(image attached below)";
|
|
33
|
+
return Array.isArray(original) && original.length > 0 ? "(non-text tool result omitted)" : "";
|
|
34
|
+
}
|
|
35
|
+
function toUserParts(content, supportsVision) {
|
|
36
|
+
if (typeof content === "string") {
|
|
37
|
+
return content === "" ? [] : [{ type: "input_text", text: content }];
|
|
38
|
+
}
|
|
39
|
+
const nDocs = content.filter((c) => c.type === "document").length;
|
|
40
|
+
if (nDocs > 0)
|
|
41
|
+
emitBrainTelemetry({ kind: "document_placeholder", count: nDocs });
|
|
42
|
+
const images = content.filter((c) => c.type === "image" && c.data !== undefined && c.mimeType !== undefined);
|
|
43
|
+
if (!supportsVision && images.length > 0)
|
|
44
|
+
emitBrainTelemetry({ kind: "vision_placeholder", count: images.length });
|
|
45
|
+
const parts = [];
|
|
46
|
+
for (const c of content) {
|
|
47
|
+
if (c.type === "text" && typeof c.text === "string" && c.text.length > 0) {
|
|
48
|
+
parts.push({ type: "input_text", text: c.text });
|
|
49
|
+
}
|
|
50
|
+
else if (c.type === "image" && c.data !== undefined && c.mimeType !== undefined) {
|
|
51
|
+
if (supportsVision)
|
|
52
|
+
parts.push({ type: "input_image", image_url: `data:${c.mimeType};base64,${c.data}` });
|
|
53
|
+
}
|
|
54
|
+
else if (c.type === "document") {
|
|
55
|
+
parts.push({ type: "input_text", text: DOCUMENT_OMITTED });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!supportsVision && images.length > 0)
|
|
59
|
+
parts.push({ type: "input_text", text: imagesOmittedNoVisionNote(images.length) });
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
function toResponsesInput(ctx, model, replayReasoning) {
|
|
63
|
+
const supportsVision = modelSupportsVision(model);
|
|
64
|
+
const out = [];
|
|
65
|
+
let pendingImages = [];
|
|
66
|
+
const flushImages = () => {
|
|
67
|
+
if (pendingImages.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const ids = [...new Set(pendingImages.map((i) => i.toolCallId))].join(", ");
|
|
70
|
+
out.push({
|
|
71
|
+
type: "message",
|
|
72
|
+
role: "user",
|
|
73
|
+
content: [
|
|
74
|
+
{ type: "input_text", text: `[Image${pendingImages.length > 1 ? "s" : ""} from tool result ${ids}]` },
|
|
75
|
+
...pendingImages.map((i) => ({ type: "input_image", image_url: `data:${i.mimeType};base64,${i.data}` })),
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
pendingImages = [];
|
|
79
|
+
};
|
|
80
|
+
for (const m of ctx.messages) {
|
|
81
|
+
if (m.role !== "toolResult")
|
|
82
|
+
flushImages();
|
|
83
|
+
if (m.role === "user") {
|
|
84
|
+
const parts = toUserParts(m.content, supportsVision);
|
|
85
|
+
out.push({
|
|
86
|
+
type: "message",
|
|
87
|
+
role: "user",
|
|
88
|
+
content: parts.length > 0 ? parts : [{ type: "input_text", text: NO_CONTENT }],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (m.role === "assistant") {
|
|
92
|
+
const toolCalls = m.content.filter((c) => c.type === "toolCall");
|
|
93
|
+
const thinking = m.content.filter((c) => c.type === "thinking");
|
|
94
|
+
const text = m.content
|
|
95
|
+
.filter((c) => c.type === "text")
|
|
96
|
+
.map((c) => c.text)
|
|
97
|
+
.join("");
|
|
98
|
+
if (replayReasoning && toolCalls.length > 0) {
|
|
99
|
+
for (const t of thinking) {
|
|
100
|
+
if (t.thinking === "")
|
|
101
|
+
continue;
|
|
102
|
+
out.push({
|
|
103
|
+
type: "reasoning",
|
|
104
|
+
...(t.thinkingSignature !== undefined ? { id: t.thinkingSignature } : {}),
|
|
105
|
+
content: [{ type: "reasoning_text", text: t.thinking }],
|
|
106
|
+
summary: [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (text !== "")
|
|
111
|
+
out.push({ type: "message", role: "assistant", content: [{ type: "output_text", text }] });
|
|
112
|
+
for (const tc of toolCalls) {
|
|
113
|
+
out.push({ type: "function_call", call_id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments ?? {}) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (m.role === "toolResult") {
|
|
117
|
+
const mediaImages = sendableImages(Array.isArray(m.content) ? m.content : []);
|
|
118
|
+
const rawImages = m.isError ? [] : mediaImages;
|
|
119
|
+
const images = supportsVision ? rawImages : [];
|
|
120
|
+
if (!supportsVision && rawImages.length > 0)
|
|
121
|
+
emitBrainTelemetry({ kind: "vision_placeholder", count: rawImages.length });
|
|
122
|
+
const nDocs = Array.isArray(m.content) ? m.content.filter((c) => c.type === "document").length : 0;
|
|
123
|
+
if (nDocs > 0)
|
|
124
|
+
emitBrainTelemetry({ kind: "document_placeholder", count: nDocs });
|
|
125
|
+
const baseText0 = textOf(m.content);
|
|
126
|
+
const baseText = nDocs > 0 ? (baseText0 ? `${baseText0}\n${DOCUMENT_OMITTED}` : DOCUMENT_OMITTED) : baseText0;
|
|
127
|
+
const withVisionNote = !supportsVision && rawImages.length > 0 ? (baseText ? `${baseText}\n${IMAGE_OMITTED_NO_VISION}` : IMAGE_OMITTED_NO_VISION) : baseText;
|
|
128
|
+
const errorNote = m.isError ? errorResultMediaNote({ images: mediaImages.length, documents: 0 }) : undefined;
|
|
129
|
+
const finalText = errorNote === undefined ? withVisionNote : withVisionNote ? `${withVisionNote}\n${errorNote}` : errorNote;
|
|
130
|
+
out.push({
|
|
131
|
+
type: "function_call_output",
|
|
132
|
+
call_id: m.toolCallId,
|
|
133
|
+
output: functionCallOutputText(finalText, images.length > 0, m.content),
|
|
134
|
+
});
|
|
135
|
+
for (const img of images)
|
|
136
|
+
pendingImages.push({ toolCallId: m.toolCallId, data: img.data, mimeType: img.mimeType });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
flushImages();
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function toResponsesTools(ctx) {
|
|
143
|
+
if (!ctx.tools || ctx.tools.length === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
return ctx.tools.map((t) => ({
|
|
146
|
+
type: "function",
|
|
147
|
+
name: t.name,
|
|
148
|
+
description: t.description,
|
|
149
|
+
parameters: t.parameters,
|
|
150
|
+
strict: false,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
function resolveWireEffort(model, reasoning) {
|
|
154
|
+
if (!model.reasoning || !isThinkingLevel(reasoning) || reasoning === "off")
|
|
155
|
+
return undefined;
|
|
156
|
+
const compat = responsesCompat(model);
|
|
157
|
+
if (compat.supportsReasoningEffort === false)
|
|
158
|
+
return undefined;
|
|
159
|
+
const effort = resolveEffort(reasoning, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS).effective;
|
|
160
|
+
const mapped = model.thinkingLevelMap?.[effort];
|
|
161
|
+
if (mapped === null)
|
|
162
|
+
return undefined;
|
|
163
|
+
return mapped ?? effort;
|
|
164
|
+
}
|
|
165
|
+
function computeUsage(model, raw) {
|
|
166
|
+
const input = raw?.input_tokens ?? 0;
|
|
167
|
+
const output = raw?.output_tokens ?? 0;
|
|
168
|
+
const total = raw?.total_tokens ?? input + output;
|
|
169
|
+
const cacheRead = raw?.input_tokens_details?.cached_tokens ?? 0;
|
|
170
|
+
const c = model.cost;
|
|
171
|
+
const costInput = (input / 1e6) * (c?.input ?? 0);
|
|
172
|
+
const costOutput = (output / 1e6) * (c?.output ?? 0);
|
|
173
|
+
return {
|
|
174
|
+
input,
|
|
175
|
+
output,
|
|
176
|
+
cacheRead,
|
|
177
|
+
cacheWrite: 0,
|
|
178
|
+
totalTokens: total,
|
|
179
|
+
cost: { input: costInput, output: costOutput, cacheRead: 0, cacheWrite: 0, total: costInput + costOutput },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function responsesErrorCode(code) {
|
|
183
|
+
switch (code) {
|
|
184
|
+
case "rate_limit_exceeded":
|
|
185
|
+
return "rate_limit";
|
|
186
|
+
case "authentication_error":
|
|
187
|
+
case "invalid_api_key":
|
|
188
|
+
case "permission_error":
|
|
189
|
+
return "auth";
|
|
190
|
+
case "invalid_request_error":
|
|
191
|
+
case "invalid_prompt":
|
|
192
|
+
return "invalid_request";
|
|
193
|
+
default:
|
|
194
|
+
return "server";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const INCOMPLETE_MAX_OUTPUT_TOKENS = "max_output_tokens";
|
|
198
|
+
const INCOMPLETE_CONTENT_FILTER = "content_filter";
|
|
199
|
+
function closeFunctionCall(acc) {
|
|
200
|
+
if (acc.closedTc)
|
|
201
|
+
return acc.closedTc;
|
|
202
|
+
if (!acc.name)
|
|
203
|
+
return undefined;
|
|
204
|
+
const args = parseStreamedToolArgs(acc.args);
|
|
205
|
+
if (args === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (!acc.callId)
|
|
208
|
+
acc.callId = mintFallbackToolCallId("call_");
|
|
209
|
+
acc.closedTc = { type: "toolCall", id: acc.callId, name: acc.name, arguments: args };
|
|
210
|
+
return acc.closedTc;
|
|
211
|
+
}
|
|
212
|
+
function mapTerminalStopReason(incompleteReason, hadToolCalls) {
|
|
213
|
+
if (hadToolCalls)
|
|
214
|
+
return "toolUse";
|
|
215
|
+
if (incompleteReason === INCOMPLETE_MAX_OUTPUT_TOKENS)
|
|
216
|
+
return "length";
|
|
217
|
+
return "stop";
|
|
218
|
+
}
|
|
219
|
+
export function createOpenResponsesBrain(config = {}) {
|
|
220
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
221
|
+
const stream = (model, context, options) => {
|
|
222
|
+
let sentMaxTokens;
|
|
223
|
+
let sentMaxTokensLane = "unset";
|
|
224
|
+
let sentEffort;
|
|
225
|
+
return runStreamingBrain({
|
|
226
|
+
model,
|
|
227
|
+
doFetch,
|
|
228
|
+
signal: options?.signal,
|
|
229
|
+
config,
|
|
230
|
+
httpLabel: "responses",
|
|
231
|
+
stallTimeouts: options?.stallTimeouts,
|
|
232
|
+
buildRequest: () => {
|
|
233
|
+
const apiKey = options?.apiKey ?? config.apiKey;
|
|
234
|
+
const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
|
|
235
|
+
if (!root) {
|
|
236
|
+
throw new Error("createOpenResponsesBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
237
|
+
}
|
|
238
|
+
if (options?.stop !== undefined && options.stop.length > 0) {
|
|
239
|
+
throw new Error("createOpenResponsesBrain: options.stop is not expressible in the Open Responses request (the wire form has no stop-sequence field); " +
|
|
240
|
+
"remove it, or serve this model through the chat-completions lane, which does support it");
|
|
241
|
+
}
|
|
242
|
+
const requestedMaxTokens = options?.maxTokens ?? model.maxTokens;
|
|
243
|
+
const effort = resolveWireEffort(model, options?.reasoning);
|
|
244
|
+
sentEffort = effort;
|
|
245
|
+
const body = {
|
|
246
|
+
model: model.id,
|
|
247
|
+
input: toResponsesInput(context, model, config.replayReasoning !== false),
|
|
248
|
+
stream: true,
|
|
249
|
+
};
|
|
250
|
+
if (context.systemPrompt)
|
|
251
|
+
body.instructions = context.systemPrompt;
|
|
252
|
+
const tools = toResponsesTools(context);
|
|
253
|
+
if (tools)
|
|
254
|
+
body.tools = tools;
|
|
255
|
+
if (options?.temperature !== undefined)
|
|
256
|
+
body.temperature = options.temperature;
|
|
257
|
+
if (requestedMaxTokens !== undefined) {
|
|
258
|
+
const capped = Math.max(1, requestedMaxTokens);
|
|
259
|
+
body.max_output_tokens = capped;
|
|
260
|
+
sentMaxTokens = capped;
|
|
261
|
+
sentMaxTokensLane = options?.maxTokens !== undefined ? "options" : "model";
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
sentMaxTokens = undefined;
|
|
265
|
+
sentMaxTokensLane = "unset";
|
|
266
|
+
}
|
|
267
|
+
if (effort !== undefined)
|
|
268
|
+
body.reasoning = { effort };
|
|
269
|
+
const headers = {
|
|
270
|
+
...model.headers,
|
|
271
|
+
...config.headers,
|
|
272
|
+
...options?.headers,
|
|
273
|
+
};
|
|
274
|
+
if (options?.apiKey !== undefined)
|
|
275
|
+
stripAuthHeaders(headers);
|
|
276
|
+
headers["content-type"] = "application/json";
|
|
277
|
+
if (apiKey)
|
|
278
|
+
headers["authorization"] = `Bearer ${apiKey}`;
|
|
279
|
+
return {
|
|
280
|
+
url: `${root}/responses`,
|
|
281
|
+
headers,
|
|
282
|
+
body: JSON.stringify(applyExtraBody(body, model.extraBody, RESPONSES_RESERVED)),
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
makeParser: (ctrl) => {
|
|
286
|
+
const { out, partial } = ctrl;
|
|
287
|
+
const items = new Map();
|
|
288
|
+
let usageRaw;
|
|
289
|
+
let responseId;
|
|
290
|
+
let terminalStatus;
|
|
291
|
+
let incompleteReason;
|
|
292
|
+
let accumRefusal = "";
|
|
293
|
+
const capabilityNotes = [];
|
|
294
|
+
let degenerate = false;
|
|
295
|
+
let degenFace;
|
|
296
|
+
let degenItem;
|
|
297
|
+
const rep = createRepetitionPoll();
|
|
298
|
+
let lastDegenCheckText = 0;
|
|
299
|
+
let lastDegenCheckReasoning = 0;
|
|
300
|
+
const detectRep = config.detectRepetition !== false;
|
|
301
|
+
let malformedFrames = 0;
|
|
302
|
+
let malformedSample;
|
|
303
|
+
const auditEcho = (env) => {
|
|
304
|
+
if (env.store === true) {
|
|
305
|
+
capabilityNotes.push("endpoint reports store=true although the request never sent `store` — server-side response state may be retained");
|
|
306
|
+
}
|
|
307
|
+
if (env.previous_response_id !== undefined && env.previous_response_id !== null) {
|
|
308
|
+
capabilityNotes.push("endpoint echoed a previous_response_id although the request never sent one — the turn may have been linked server-side");
|
|
309
|
+
}
|
|
310
|
+
if (env.conversation !== undefined && env.conversation !== null) {
|
|
311
|
+
capabilityNotes.push("endpoint echoed a conversation binding although the request never sent one — the turn may have been linked server-side");
|
|
312
|
+
}
|
|
313
|
+
if (sentEffort !== undefined) {
|
|
314
|
+
const echoed = env.reasoning?.effort;
|
|
315
|
+
if (echoed !== sentEffort) {
|
|
316
|
+
capabilityNotes.push(`reasoning effort "${sentEffort}" was requested but the endpoint reports ${echoed === null || echoed === undefined ? "none" : `"${echoed}"`}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (env.parallel_tool_calls === false) {
|
|
320
|
+
capabilityNotes.push("endpoint reports parallel_tool_calls=false — tool calls will arrive one per turn on this lane");
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const openItem = (index, kind, itemId) => {
|
|
324
|
+
const acc = { kind, text: "", itemId, callId: "", name: "", args: "", closed: false };
|
|
325
|
+
items.set(index, acc);
|
|
326
|
+
return acc;
|
|
327
|
+
};
|
|
328
|
+
const pushReasoningDelta = (acc, index, delta) => {
|
|
329
|
+
if (delta === "")
|
|
330
|
+
return;
|
|
331
|
+
ctrl.sawContentToken();
|
|
332
|
+
if (acc.closed)
|
|
333
|
+
return;
|
|
334
|
+
if (!acc.block) {
|
|
335
|
+
acc.block = { type: "thinking", thinking: "" };
|
|
336
|
+
partial.content = [acc.block];
|
|
337
|
+
out.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } });
|
|
338
|
+
}
|
|
339
|
+
acc.text += delta;
|
|
340
|
+
if (acc.block.type === "thinking")
|
|
341
|
+
acc.block.thinking = acc.text;
|
|
342
|
+
partial.content = [acc.block];
|
|
343
|
+
out.push({ type: "thinking_delta", contentIndex: index, delta, partial: { ...partial } });
|
|
344
|
+
if (detectRep && !degenerate && acc.text.length - lastDegenCheckReasoning >= DEGENERATE_POLL_CHARS) {
|
|
345
|
+
lastDegenCheckReasoning = acc.text.length;
|
|
346
|
+
if (rep.poll(acc.text)) {
|
|
347
|
+
degenerate = true;
|
|
348
|
+
degenFace = "reasoning";
|
|
349
|
+
degenItem = acc;
|
|
350
|
+
ctrl.cancel();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const pushTextDelta = (acc, index, delta) => {
|
|
355
|
+
if (delta === "")
|
|
356
|
+
return;
|
|
357
|
+
ctrl.sawContentToken();
|
|
358
|
+
if (acc.closed)
|
|
359
|
+
return;
|
|
360
|
+
if (!acc.block) {
|
|
361
|
+
acc.block = { type: "text", text: "" };
|
|
362
|
+
partial.content = [acc.block];
|
|
363
|
+
out.push({ type: "text_start", contentIndex: index, partial: { ...partial } });
|
|
364
|
+
}
|
|
365
|
+
acc.text += delta;
|
|
366
|
+
if (acc.block.type === "text")
|
|
367
|
+
acc.block.text = acc.text;
|
|
368
|
+
partial.content = [acc.block];
|
|
369
|
+
out.push({ type: "text_delta", contentIndex: index, delta, partial: { ...partial } });
|
|
370
|
+
if (detectRep && !degenerate && acc.text.length - lastDegenCheckText >= DEGENERATE_POLL_CHARS) {
|
|
371
|
+
lastDegenCheckText = acc.text.length;
|
|
372
|
+
if (rep.poll(acc.text)) {
|
|
373
|
+
degenerate = true;
|
|
374
|
+
degenFace = "text";
|
|
375
|
+
degenItem = acc;
|
|
376
|
+
ctrl.cancel();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const closeItem = (index, acc) => {
|
|
381
|
+
if (acc.closed)
|
|
382
|
+
return;
|
|
383
|
+
acc.closed = true;
|
|
384
|
+
if (acc.kind === "reasoning") {
|
|
385
|
+
if (acc.block)
|
|
386
|
+
out.push({ type: "thinking_end", contentIndex: index, content: acc.text, partial: { ...partial } });
|
|
387
|
+
}
|
|
388
|
+
else if (acc.kind === "message") {
|
|
389
|
+
if (acc.block)
|
|
390
|
+
out.push({ type: "text_end", contentIndex: index, content: acc.text, partial: { ...partial } });
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const tc = closeFunctionCall(acc);
|
|
394
|
+
if (tc)
|
|
395
|
+
out.push({ type: "toolcall_end", contentIndex: index, toolCall: tc, partial: { ...partial } });
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const handleFrame = (frame) => {
|
|
399
|
+
const type = frame.type;
|
|
400
|
+
if (type === undefined)
|
|
401
|
+
return;
|
|
402
|
+
switch (type) {
|
|
403
|
+
case "response.created": {
|
|
404
|
+
const env = frame.response;
|
|
405
|
+
if (env) {
|
|
406
|
+
if (env.id !== undefined)
|
|
407
|
+
responseId = env.id;
|
|
408
|
+
if (env.model !== undefined && env.model !== model.id)
|
|
409
|
+
partial.responseModel = env.model;
|
|
410
|
+
auditEcho(env);
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
case "response.output_item.added": {
|
|
415
|
+
ctrl.sawContentToken();
|
|
416
|
+
const index = frame.output_index;
|
|
417
|
+
if (index === undefined)
|
|
418
|
+
return;
|
|
419
|
+
const itemType = frame.item?.type;
|
|
420
|
+
const itemId = frame.item?.id ?? "";
|
|
421
|
+
if (itemType === "reasoning") {
|
|
422
|
+
openItem(index, "reasoning", itemId);
|
|
423
|
+
}
|
|
424
|
+
else if (itemType === "message") {
|
|
425
|
+
openItem(index, "message", itemId);
|
|
426
|
+
}
|
|
427
|
+
else if (itemType === "function_call") {
|
|
428
|
+
const acc = openItem(index, "function_call", itemId);
|
|
429
|
+
acc.callId = frame.item?.call_id ?? "";
|
|
430
|
+
acc.name = frame.item?.name ?? "";
|
|
431
|
+
if (frame.item?.arguments)
|
|
432
|
+
acc.args += frame.item.arguments;
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
case "response.reasoning_text.delta": {
|
|
437
|
+
const index = frame.output_index;
|
|
438
|
+
if (index === undefined || frame.delta === undefined)
|
|
439
|
+
return;
|
|
440
|
+
const acc = items.get(index) ?? openItem(index, "reasoning", "");
|
|
441
|
+
pushReasoningDelta(acc, index, frame.delta);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
case "response.output_text.delta": {
|
|
445
|
+
const index = frame.output_index;
|
|
446
|
+
if (index === undefined || frame.delta === undefined)
|
|
447
|
+
return;
|
|
448
|
+
const acc = items.get(index) ?? openItem(index, "message", "");
|
|
449
|
+
pushTextDelta(acc, index, frame.delta);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
case "response.refusal.delta": {
|
|
453
|
+
if (frame.delta === undefined)
|
|
454
|
+
return;
|
|
455
|
+
ctrl.sawContentToken();
|
|
456
|
+
accumRefusal += frame.delta;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
case "response.refusal.done": {
|
|
460
|
+
if (frame.refusal === undefined || accumRefusal !== "")
|
|
461
|
+
return;
|
|
462
|
+
ctrl.sawContentToken();
|
|
463
|
+
accumRefusal = frame.refusal;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
case "response.reasoning_text.done": {
|
|
467
|
+
const index = frame.output_index;
|
|
468
|
+
if (index === undefined || frame.text === undefined)
|
|
469
|
+
return;
|
|
470
|
+
const acc = items.get(index) ?? openItem(index, "reasoning", "");
|
|
471
|
+
if (acc.text === "")
|
|
472
|
+
pushReasoningDelta(acc, index, frame.text);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
case "response.output_text.done": {
|
|
476
|
+
const index = frame.output_index;
|
|
477
|
+
if (index === undefined || frame.text === undefined)
|
|
478
|
+
return;
|
|
479
|
+
const acc = items.get(index) ?? openItem(index, "message", "");
|
|
480
|
+
if (acc.text === "")
|
|
481
|
+
pushTextDelta(acc, index, frame.text);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
case "response.function_call_arguments.delta": {
|
|
485
|
+
const index = frame.output_index;
|
|
486
|
+
if (index === undefined || frame.delta === undefined)
|
|
487
|
+
return;
|
|
488
|
+
const acc = items.get(index);
|
|
489
|
+
if (!acc || acc.kind !== "function_call")
|
|
490
|
+
return;
|
|
491
|
+
ctrl.sawContentToken();
|
|
492
|
+
if (!acc.closed)
|
|
493
|
+
acc.args += frame.delta;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
case "response.function_call_arguments.done": {
|
|
497
|
+
const index = frame.output_index;
|
|
498
|
+
if (index === undefined || frame.arguments === undefined)
|
|
499
|
+
return;
|
|
500
|
+
const acc = items.get(index);
|
|
501
|
+
if (!acc || acc.kind !== "function_call" || acc.closed)
|
|
502
|
+
return;
|
|
503
|
+
ctrl.sawContentToken();
|
|
504
|
+
if (acc.args === "")
|
|
505
|
+
acc.args = frame.arguments;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
case "response.output_item.done": {
|
|
509
|
+
const index = frame.output_index;
|
|
510
|
+
if (index === undefined)
|
|
511
|
+
return;
|
|
512
|
+
const acc = items.get(index);
|
|
513
|
+
if (!acc)
|
|
514
|
+
return;
|
|
515
|
+
if (acc.kind === "function_call") {
|
|
516
|
+
if (!acc.callId && frame.item?.call_id)
|
|
517
|
+
acc.callId = frame.item.call_id;
|
|
518
|
+
if (!acc.name && frame.item?.name)
|
|
519
|
+
acc.name = frame.item.name;
|
|
520
|
+
if (acc.args === "" && frame.item?.arguments)
|
|
521
|
+
acc.args = frame.item.arguments;
|
|
522
|
+
}
|
|
523
|
+
if (!acc.itemId && frame.item?.id)
|
|
524
|
+
acc.itemId = frame.item.id;
|
|
525
|
+
closeItem(index, acc);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
case "response.completed":
|
|
529
|
+
case "response.incomplete": {
|
|
530
|
+
const env = frame.response;
|
|
531
|
+
terminalStatus = env?.status ?? (type === "response.completed" ? "completed" : "incomplete");
|
|
532
|
+
incompleteReason = env?.incomplete_details?.reason;
|
|
533
|
+
if (env?.usage)
|
|
534
|
+
usageRaw = env.usage;
|
|
535
|
+
if (env?.id !== undefined)
|
|
536
|
+
responseId = env.id;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
case "response.failed": {
|
|
540
|
+
const err = frame.response?.error;
|
|
541
|
+
throw new BrainError(responsesErrorCode(err?.code), `responses stream failed (${err?.code ?? "unknown"}): ${err?.message ?? ""}`);
|
|
542
|
+
}
|
|
543
|
+
case "error": {
|
|
544
|
+
throw new BrainError(responsesErrorCode(frame.code), `responses stream error (${frame.code ?? "unknown"}): ${frame.message ?? ""}`);
|
|
545
|
+
}
|
|
546
|
+
default:
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
return {
|
|
551
|
+
snapshot() {
|
|
552
|
+
const accs = [...items.values()];
|
|
553
|
+
const hasCompletedToolCall = accs.some((a) => a.closedTc !== undefined);
|
|
554
|
+
const hasSubstantiveText = accs.some((a) => a.kind === "message" && a.text.trim() !== "") || accumRefusal.trim() !== "";
|
|
555
|
+
const hasReasoning = accs.some((a) => a.kind === "reasoning" && a.text.length > 0);
|
|
556
|
+
return {
|
|
557
|
+
hasSubstantiveText,
|
|
558
|
+
hasCompletedToolCall,
|
|
559
|
+
hasOnlyThinking: hasReasoning && !hasSubstantiveText && !hasCompletedToolCall,
|
|
560
|
+
};
|
|
561
|
+
},
|
|
562
|
+
sealForRetry() {
|
|
563
|
+
for (const [index, acc] of [...items.entries()].sort((a, b) => a[0] - b[0])) {
|
|
564
|
+
if (acc.kind === "reasoning" && !acc.closed) {
|
|
565
|
+
acc.closed = true;
|
|
566
|
+
if (acc.block)
|
|
567
|
+
out.push({ type: "thinking_end", contentIndex: index, content: acc.text, partial: { ...partial } });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
onLine(rawLine) {
|
|
572
|
+
const line = rawLine.trim();
|
|
573
|
+
if (!line.startsWith("data:"))
|
|
574
|
+
return;
|
|
575
|
+
const data = line.slice(5).trim();
|
|
576
|
+
if (!data)
|
|
577
|
+
return;
|
|
578
|
+
let frame;
|
|
579
|
+
try {
|
|
580
|
+
frame = JSON.parse(data);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
malformedFrames++;
|
|
584
|
+
malformedSample ??= data.slice(0, MALFORMED_SAMPLE_CHARS);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
handleFrame(frame);
|
|
588
|
+
},
|
|
589
|
+
finalize() {
|
|
590
|
+
for (const [index, acc] of [...items.entries()].sort((a, b) => a[0] - b[0])) {
|
|
591
|
+
if (acc.closed)
|
|
592
|
+
continue;
|
|
593
|
+
if (acc.kind === "function_call") {
|
|
594
|
+
acc.closed = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
closeItem(index, acc);
|
|
598
|
+
}
|
|
599
|
+
const finalContent = [];
|
|
600
|
+
const toolCalls = [];
|
|
601
|
+
const malformed = [];
|
|
602
|
+
const unnamed = [];
|
|
603
|
+
let anyText = false;
|
|
604
|
+
let reasoningSeen = false;
|
|
605
|
+
for (const [, acc] of [...items.entries()].sort((a, b) => a[0] - b[0])) {
|
|
606
|
+
if (acc.kind === "reasoning") {
|
|
607
|
+
if (acc.text.length === 0)
|
|
608
|
+
continue;
|
|
609
|
+
reasoningSeen = true;
|
|
610
|
+
finalContent.push({
|
|
611
|
+
type: "thinking",
|
|
612
|
+
thinking: acc.text,
|
|
613
|
+
...(acc.itemId !== "" ? { thinkingSignature: acc.itemId } : {}),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
else if (acc.kind === "message") {
|
|
617
|
+
const text = acc === degenItem && degenFace === "text" && rep.cut !== undefined ? trimDegenerateTail(acc.text, rep.cut) : acc.text;
|
|
618
|
+
if (text !== "") {
|
|
619
|
+
finalContent.push({ type: "text", text });
|
|
620
|
+
if (text.trim() !== "")
|
|
621
|
+
anyText = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const tc = closeFunctionCall(acc);
|
|
626
|
+
if (tc) {
|
|
627
|
+
toolCalls.push(tc);
|
|
628
|
+
finalContent.push(tc);
|
|
629
|
+
}
|
|
630
|
+
else if (acc.name) {
|
|
631
|
+
malformed.push(`${acc.name}(${acc.args.slice(0, MALFORMED_ARGS_CHARS)})`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
unnamed.push(`call_id="${acc.callId || "?"}"(${acc.args.slice(0, MALFORMED_ARGS_CHARS)})`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const noUsableContent = toolCalls.length === 0 && !anyText;
|
|
639
|
+
const terminalLabel = terminalStatus ?? "?";
|
|
640
|
+
const toolErrorParts = [];
|
|
641
|
+
if (malformed.length > 0) {
|
|
642
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, status="${terminalLabel}"): ${malformed.join("; ")}`);
|
|
643
|
+
}
|
|
644
|
+
if (unnamed.length > 0) {
|
|
645
|
+
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (status="${terminalLabel}"): ${unnamed.join("; ")}`);
|
|
646
|
+
}
|
|
647
|
+
const toolError = toolErrorParts.length > 0 ? toolErrorParts.join(" | ") : undefined;
|
|
648
|
+
if (malformed.length > 0) {
|
|
649
|
+
finalContent.push({
|
|
650
|
+
type: "text",
|
|
651
|
+
text: `\n[note: ${malformed.length} tool call(s) were truncated (status="${terminalLabel}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (unnamed.length > 0) {
|
|
655
|
+
finalContent.push({
|
|
656
|
+
type: "text",
|
|
657
|
+
text: `\n[note: ${unnamed.length} tool call(s) arrived with no tool name and were dropped — re-issue them next turn with an explicit tool name: ${unnamed.join("; ")}]`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (malformedFrames > 0) {
|
|
661
|
+
finalContent.push({
|
|
662
|
+
type: "text",
|
|
663
|
+
text: `\n[note: ${malformedFrames} provider stream frame(s) were not valid JSON and were dropped — the streamed output may be missing content (first sample: ${JSON.stringify(malformedSample)})]`,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const truncated = incompleteReason === INCOMPLETE_MAX_OUTPUT_TOKENS;
|
|
667
|
+
const truncatedEmpty = truncated && noUsableContent && !toolError;
|
|
668
|
+
const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
|
|
669
|
+
const staticReasoningCut = truncatedEmpty && !dynamicCut && reasoningSeen && options?.staticReasoningCutDowngrade === true;
|
|
670
|
+
const safetyCut = incompleteReason === INCOMPLETE_CONTENT_FILTER;
|
|
671
|
+
const refused = accumRefusal.trim().length > 0;
|
|
672
|
+
const emptyNoFinish = noUsableContent && terminalStatus === undefined && !degenerate;
|
|
673
|
+
const maxTokensNote = sentMaxTokens !== undefined
|
|
674
|
+
? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : "model.maxTokens"})`
|
|
675
|
+
: "max_output_tokens not set (neither options.maxTokens nor model.maxTokens — the endpoint's default cap applied)";
|
|
676
|
+
const errorMessage = degenerate
|
|
677
|
+
? DEGENERATE_MESSAGE
|
|
678
|
+
: safetyCut
|
|
679
|
+
? `[refusal] response cut by the provider content filter (incomplete_details.reason="${INCOMPLETE_CONTENT_FILTER}") — the output was censored/truncated for policy and is unreliable`
|
|
680
|
+
: refused
|
|
681
|
+
? `[refusal] response refused by the model for policy reasons: ${accumRefusal.trim()}`
|
|
682
|
+
: (toolError ??
|
|
683
|
+
(truncatedEmpty && !dynamicCut && !staticReasoningCut
|
|
684
|
+
? reasoningSeen
|
|
685
|
+
? `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning; raise the cap, or lower the reasoning effort for this task.`
|
|
686
|
+
: `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — raise the cap`
|
|
687
|
+
: emptyNoFinish
|
|
688
|
+
? `[stream_torn] model stream ended with no content and no terminal response event — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
|
|
689
|
+
: undefined));
|
|
690
|
+
const errored = degenerate ||
|
|
691
|
+
safetyCut ||
|
|
692
|
+
refused ||
|
|
693
|
+
emptyNoFinish ||
|
|
694
|
+
(toolError !== undefined && noUsableContent) ||
|
|
695
|
+
(truncatedEmpty && !dynamicCut && !staticReasoningCut);
|
|
696
|
+
const errorKind = degenerate
|
|
697
|
+
? "degenerate"
|
|
698
|
+
: !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
|
|
699
|
+
? "length_empty"
|
|
700
|
+
: undefined;
|
|
701
|
+
const diagnostics = capabilityNotes.length > 0
|
|
702
|
+
? [...(partial.diagnostics ?? []), { type: CAPABILITY_DIAGNOSTIC_TYPE, timestamp: Date.now(), details: { notes: [...capabilityNotes] } }]
|
|
703
|
+
: [];
|
|
704
|
+
const finalMessage = {
|
|
705
|
+
...partial,
|
|
706
|
+
content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
|
|
707
|
+
stopReason: errored ? "error" : mapTerminalStopReason(incompleteReason, toolCalls.length > 0),
|
|
708
|
+
usage: computeUsage(model, usageRaw),
|
|
709
|
+
...(usageRaw === undefined ? { usageMissing: true } : {}),
|
|
710
|
+
timestamp: Date.now(),
|
|
711
|
+
...(responseId !== undefined ? { responseId } : {}),
|
|
712
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
713
|
+
...(errorKind ? { errorKind } : {}),
|
|
714
|
+
...(rep.cut !== undefined || rep.spared.length > 0
|
|
715
|
+
? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
|
|
716
|
+
: {}),
|
|
717
|
+
...(staticReasoningCut ? { staticReasoningCut: true } : {}),
|
|
718
|
+
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
|
719
|
+
};
|
|
720
|
+
if (errored) {
|
|
721
|
+
out.push({ type: "error", reason: "error", error: finalMessage });
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
out.push({
|
|
725
|
+
type: "done",
|
|
726
|
+
reason: finalMessage.stopReason,
|
|
727
|
+
message: finalMessage,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
return { stream };
|
|
736
|
+
}
|