refactor(pi-devin): switch from npm+patch to vendored local fork

- Vendor the patched pi-devin@0.1.2 tree at apps/pi-devin/upstream/
  (third-party archive -> upstream_fork); git history replaces the patch
  layer as the change archive. Includes the devin-desktop identity fix
  and normalized catalog variant mapping from the previous patch.
- build-pi-package.sh now verifies source.sha256 against
  host-local-packages.json and npm-packs the vendored tree; drop the
  npm fetch + patch stage.
- install-pi-package.sh strips the npm-pack package/ prefix to match
  the playbook's unarchive behavior.
- Deployed to a100 via deploy_pi_devin_package (--limit a100, R2 then
  R3); remote verified to carry the fork source.
This commit is contained in:
Shujakuinkuraudo
2026-09-15 12:08:30 -04:00
committed by shujakuin
parent 851c361a84
commit 654221119d
22 changed files with 690 additions and 1187 deletions
+307 -178
View File
@@ -4,15 +4,15 @@ import {
type Api,
type AssistantMessage,
type AssistantMessageEventStream,
type Context,
type Model,
type SimpleStreamOptions,
type TranscriptContext,
calculateCost,
createAssistantMessageEventStream,
parseStreamingJson,
} from "@earendil-works/pi-ai";
import { mapContextToChat, type ChatHistoryItem, type ContentPart, type ToolDef } from "./context-map.js";
import { getCachedUserJwt } from "./jwt.js";
import { clampMaxTokensToContext } from "./context-estimate.js";
import { mapContextToChat, type ChatHistoryItem, type ToolDef } from "./context-map.js";
import { anySignal, getCachedUserJwt } from "./jwt.js";
import { buildMetadata } from "./metadata.js";
import { resolveModelUid } from "./models.js";
import {
@@ -24,32 +24,40 @@ import {
iterFields,
} from "./wire.js";
const SOURCE_BY_ROLE: Record<ChatHistoryItem["role"], number> = {
const SOURCE_BY_ROLE: Record<string, number> = {
user: 1,
assistant: 2,
tool: 4,
};
/** Connect frame payloads larger than this indicate a corrupt length prefix. */
const MAX_CONNECT_FRAME_PAYLOAD = 16 * 1024 * 1024;
/**
* Recovery heuristic for opaque Devin `invalid_argument`/`internal error`
* trailers: compactable message history this large is likely the cause, so the
* failure is reported in a shape Pi recognizes as context overflow.
*/
const LARGE_HISTORY_RECOVERY_BYTES = 512 * 1024;
export type CloudChatEvent =
| { kind: "text"; text: string }
| { kind: "reasoning"; text: string }
| { kind: "signature"; text: string }
| { kind: "thinking_redacted" }
| { kind: "message_id"; id: string }
| { kind: "actual_model"; uid: string }
| { kind: "tool_call_start"; id: string; name: string }
| { kind: "tool_call_args"; argsDelta: string; id?: string }
| { kind: "finish"; reason: "stop" | "tool_calls" | "length" | "content_filter" }
| { kind: "finish"; reason: "stop" | "tool_calls" | "length" | "content_filter" | "error" }
| {
kind: "usage";
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
cachedInputTokens?: number;
cacheCreationInputTokens?: number;
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
modelUid?: string;
};
function normalizeContent(content: string | ContentPart[]): ContentPart[] {
if (typeof content === "string") return [{ type: "text", text: content }];
return content;
}
function encodeImageData(img: { mimeType?: string; base64Data?: string }): Buffer {
return Buffer.concat([
encodeString(1, img.base64Data ?? ""),
@@ -61,39 +69,41 @@ function encodeChatToolCall(tc: { id: string; name: string; arguments: string })
return Buffer.concat([encodeString(1, tc.id), encodeString(2, tc.name), encodeString(3, tc.arguments)]);
}
function encodeChatMessagePrompt(
content: ContentPart[],
source: number,
opts?: { toolCallId?: string; toolCalls?: Array<{ id: string; name: string; arguments: string }> },
): Buffer {
const text = content
export function encodeChatMessagePrompt(item: ChatHistoryItem, messageId: string): Buffer {
const text = item.content
.filter((part) => part.type === "text")
.map((part) => part.text ?? "")
.join("\n");
const parts: Buffer[] = [
encodeVarintField(2, source),
encodeString(1, messageId),
encodeVarintField(2, SOURCE_BY_ROLE[item.role] ?? 1),
encodeString(3, text),
encodeVarintField(4, Math.max(1, Math.floor(text.length / 4))),
encodeVarintField(5, 1),
];
if (opts?.toolCallId) parts.push(encodeString(7, opts.toolCallId));
for (const tc of opts?.toolCalls ?? []) parts.push(encodeMessage(6, encodeChatToolCall(tc)));
for (const img of content.filter((part) => part.type === "image")) {
for (const tc of item.tool_calls ?? []) parts.push(encodeMessage(6, encodeChatToolCall(tc)));
if (item.tool_call_id) parts.push(encodeString(7, item.tool_call_id));
if (item.tool_result_is_error) parts.push(encodeVarintField(9, 1));
for (const img of item.content.filter((part) => part.type === "image")) {
parts.push(encodeMessage(10, encodeImageData(img)));
}
if (item.thinking) parts.push(encodeString(11, item.thinking));
if (item.signature) parts.push(encodeString(12, item.signature));
if (item.thinking_redacted) parts.push(encodeVarintField(13, 1));
return Buffer.concat(parts);
}
function encodeCompletionConfiguration(maxOutputTokens?: number): Buffer {
function encodeCompletionConfiguration(args: {
maxTokens: number;
temperature: number;
topP: number;
topK: number;
}): Buffer {
return Buffer.concat([
encodeVarintField(1, 1),
encodeVarintField(2, 64_000),
encodeVarintField(3, maxOutputTokens ?? 128_000),
encodeFixed64Field(5, 0.7),
encodeFixed64Field(6, 0.95),
encodeVarintField(7, 50),
encodeFixed64Field(8, 1.0),
encodeFixed64Field(11, 1.0),
encodeVarintField(2, args.maxTokens),
encodeVarintField(3, 400),
encodeFixed64Field(5, args.temperature),
encodeVarintField(7, args.topK),
encodeFixed64Field(8, args.topP),
]);
}
@@ -106,7 +116,7 @@ function encodeToolDef(tool: ToolDef): Buffer {
]);
}
function buildGetChatMessageRequest(args: {
export interface WireRequestArgs {
apiKey: string;
userJwt: string;
modelUid: string;
@@ -118,8 +128,13 @@ function buildGetChatMessageRequest(args: {
sessionId: string;
requestId: bigint;
triggerId: string;
maxOutputTokens?: number;
}): Buffer {
maxTokens: number;
temperature: number;
topP: number;
topK: number;
}
export function buildGetChatMessageRequest(args: WireRequestArgs): { body: Buffer; historyBytes: number } {
const metadata = buildMetadata({
apiKey: args.apiKey,
userJwt: args.userJwt,
@@ -127,36 +142,46 @@ function buildGetChatMessageRequest(args: {
requestId: args.requestId,
triggerId: args.triggerId,
});
const prompts = args.messages.map((message) =>
encodeMessage(
3,
encodeChatMessagePrompt(normalizeContent(message.content), SOURCE_BY_ROLE[message.role], {
toolCallId: message.role === "tool" ? message.tool_call_id : undefined,
toolCalls: message.role === "assistant" ? message.tool_calls : undefined,
}),
),
);
return Buffer.concat([
const promptFields: Buffer[] = [];
args.messages.forEach((message, index) => {
const messageId = message.message_id ?? `${args.cascadeId}-${index}`;
promptFields.push(encodeMessage(3, encodeChatMessagePrompt(message, messageId)));
});
const history = Buffer.concat(promptFields);
const body = Buffer.concat([
encodeMessage(1, metadata),
...(args.systemPrompt ? [encodeString(2, args.systemPrompt)] : []),
...prompts,
args.systemPrompt ? encodeString(2, args.systemPrompt) : Buffer.alloc(0),
history,
encodeVarintField(7, 5),
encodeMessage(8, encodeCompletionConfiguration(args.maxOutputTokens)),
encodeMessage(8, encodeCompletionConfiguration(args)),
...(args.tools ?? []).map((tool) => encodeMessage(10, encodeToolDef(tool))),
encodeString(16, args.cascadeId),
encodeString(17, args.promptId),
encodeVarintField(20, 1),
encodeString(21, args.modelUid),
encodeString(22, args.promptId),
]);
return { body, historyBytes: history.length };
}
function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
export function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
for (const field of iterFields(proto)) {
if (field.num === 3 && field.wire === 2 && Buffer.isBuffer(field.value)) {
if (field.num === 1 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const id = field.value.toString("utf8");
if (id) yield { kind: "message_id", id };
} else if (field.num === 3 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const text = field.value.toString("utf8");
if (text) yield { kind: "text", text };
} else if (field.num === 9 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const text = field.value.toString("utf8");
if (text) yield { kind: "reasoning", text };
} else if (field.num === 10 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const signature = field.value.toString("utf8");
if (signature) yield { kind: "signature", text: signature };
} else if (field.num === 11 && field.wire === 0 && field.value) {
yield { kind: "thinking_redacted" };
} else if (field.num === 23 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const uid = field.value.toString("utf8");
if (uid) yield { kind: "actual_model", uid };
} else if (field.num === 6 && field.wire === 2 && Buffer.isBuffer(field.value)) {
let id: string | undefined;
let name: string | undefined;
@@ -173,59 +198,77 @@ function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
if (argsDelta !== undefined) yield { kind: "tool_call_args", argsDelta, ...(id ? { id } : {}) };
} else if (field.num === 5 && field.wire === 0) {
const value = Number(field.value);
let reason: Extract<CloudChatEvent, { kind: "finish" }>['reason'] = "stop";
let reason: Extract<CloudChatEvent, { kind: "finish" }>["reason"] = "stop";
if (value === 10) reason = "tool_calls";
else if (value === 1 || value === 3 || value === 5) reason = "length";
else if (value === 11) reason = "content_filter";
else if (value === 1 || value === 3) reason = "length";
else if (value === 12 || value === 13) reason = "error";
yield { kind: "finish", reason };
} else if (field.num === 28 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const usage = decodeUsage(field.value);
} else if (field.num === 7 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const usage = decodeUsageStats(field.value);
if (usage) yield usage;
}
}
}
function decodeUsage(buf: Buffer): CloudChatEvent | null {
let promptTokens: number | undefined;
let completionTokens: number | undefined;
let cachedInputTokens: number | undefined;
let cacheCreationInputTokens: number | undefined;
function decodeUsageStats(buf: Buffer): CloudChatEvent | null {
let input: number | undefined;
let output: number | undefined;
let cacheWrite: number | undefined;
let cacheRead: number | undefined;
let modelUid: string | undefined;
for (const field of iterFields(buf)) {
if (field.num !== 2 || field.wire !== 2 || !Buffer.isBuffer(field.value)) continue;
let metric: string | undefined;
let value: number | undefined;
for (const inner of iterFields(field.value)) {
if (inner.num === 5 && inner.wire === 2 && Buffer.isBuffer(inner.value)) {
metric = inner.value.toString("utf8");
} else if (inner.num === 4 && inner.wire === 2 && Buffer.isBuffer(inner.value)) {
for (const dim of iterFields(inner.value)) {
if (dim.num === 2 && dim.wire === 5 && Buffer.isBuffer(dim.value)) {
value = dim.value.readFloatLE(0);
}
}
}
if (field.wire === 0) {
const n = Number(field.value);
if (field.num === 2) input = n;
else if (field.num === 3) output = n;
else if (field.num === 4) cacheWrite = n;
else if (field.num === 5) cacheRead = n;
} else if (field.num === 9 && field.wire === 2 && Buffer.isBuffer(field.value)) {
modelUid = field.value.toString("utf8");
}
if (!metric || value === undefined || !Number.isFinite(value)) continue;
const n = Math.round(value);
if (metric === "input_tokens") promptTokens = n;
else if (metric === "output_tokens") completionTokens = n;
else if (metric.includes("cached") || metric.includes("cache_read")) cachedInputTokens = n;
else if (metric.includes("cache_creation")) cacheCreationInputTokens = n;
}
if ([promptTokens, completionTokens, cachedInputTokens, cacheCreationInputTokens].every((n) => n === undefined)) return null;
return {
kind: "usage",
promptTokens,
completionTokens,
totalTokens: (promptTokens ?? 0) + (completionTokens ?? 0) + (cachedInputTokens ?? 0) + (cacheCreationInputTokens ?? 0),
cachedInputTokens,
cacheCreationInputTokens,
};
if (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined) {
return null;
}
return { kind: "usage", input, output, cacheRead, cacheWrite, modelUid };
}
export interface TrailerError {
code: string;
message: string;
}
export function readTrailerError(text: string): TrailerError | null {
if (!text.includes('"error"')) return null;
try {
const parsed = JSON.parse(text) as { error?: { code?: unknown; message?: unknown } };
if (!parsed.error || typeof parsed.error !== "object") return null;
return {
code: typeof parsed.error.code === "string" ? parsed.error.code : "",
message: typeof parsed.error.message === "string" ? parsed.error.message : "",
};
} catch {
return { code: "", message: text.slice(0, 512) };
}
}
const sessionCache = new Map<string, { sessionId: string; cascadeId: string }>();
function sessionIds(apiKey: string, host: string) {
export function sessionIds(
apiKey: string,
host: string,
options?: { sessionId?: string; cacheRetention?: string },
): { sessionId: string; cascadeId: string } {
// Compaction and other no-cache callers must not share or reuse a cascade.
if (options?.cacheRetention === "none") {
return { sessionId: randomUUID(), cascadeId: randomUUID() };
}
// Pi threads its session id through so each session tree keeps its own
// Devin cascade instead of sharing one process-wide cascade.
if (options?.sessionId) {
return { sessionId: options.sessionId, cascadeId: options.sessionId };
}
const key = `${host}\x1f${apiKey}`;
let ids = sessionCache.get(key);
if (!ids) {
@@ -235,45 +278,41 @@ function sessionIds(apiKey: string, host: string) {
return ids;
}
function mergeHeaders(extra: Record<string, string | null> | undefined): Record<string, string> {
const headers = new Map<string, string>([
["content-type", "application/connect+proto"],
["connect-protocol-version", "1"],
["connect-accept-encoding", "gzip"],
["user-agent", "connect-go/1.18.1 (go1.26.3)"],
]);
for (const [name, value] of Object.entries(extra ?? {})) {
if (value == null) headers.delete(name.toLowerCase());
else headers.set(name.toLowerCase(), value);
}
return Object.fromEntries(headers);
}
async function* streamChatEvents(args: {
apiKey: string;
host: string;
modelUid: string;
systemPrompt?: string;
messages: ChatHistoryItem[];
tools?: ToolDef[];
maxOutputTokens?: number;
request: { body: Buffer; historyBytes: number };
headers?: Record<string, string | null>;
signal?: AbortSignal;
fetchFn?: typeof fetch;
onResponse?: (response: { status: number; headers: Record<string, string> }) => void | Promise<void>;
}): AsyncGenerator<CloudChatEvent> {
const host = args.host.replace(/\/$/, "");
const userJwt = await getCachedUserJwt(args.apiKey, host, args.signal);
const ids = sessionIds(args.apiKey, host);
const proto = buildGetChatMessageRequest({
apiKey: args.apiKey,
userJwt,
modelUid: args.modelUid,
systemPrompt: args.systemPrompt,
messages: args.messages,
tools: args.tools,
cascadeId: ids.cascadeId,
promptId: randomUUID(),
sessionId: ids.sessionId,
requestId: BigInt(Date.now()),
triggerId: randomUUID(),
maxOutputTokens: args.maxOutputTokens,
});
const resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
method: "POST",
headers: {
"Content-Type": "application/connect+proto",
"Connect-Protocol-Version": "1",
"Connect-Content-Encoding": "gzip",
"Connect-Accept-Encoding": "gzip",
const resp = await (args.fetchFn ?? fetch)(
`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`,
{
method: "POST",
headers: mergeHeaders(args.headers),
body: new Uint8Array(frameConnectStream(args.request.body, false)),
signal: args.signal,
},
body: new Uint8Array(frameConnectStream(proto, true)),
signal: args.signal,
});
);
if (args.onResponse) {
await args.onResponse({ status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
}
if (!resp.ok) {
throw new Error(`GetChatMessage HTTP ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
}
@@ -283,7 +322,8 @@ async function* streamChatEvents(args: {
const queue: Buffer[] = [];
let queued = 0;
let sawEos = false;
let trailerError: string | null = null;
let producedOutput = false;
let trailerError: TrailerError | null = null;
const peek = (n: number): Buffer | null => {
if (queued < n) return null;
@@ -331,6 +371,9 @@ async function* streamChatEvents(args: {
if (!header) break;
const flags = header[0];
const len = header.readUInt32BE(1);
if (len > MAX_CONNECT_FRAME_PAYLOAD) {
throw new Error(`Devin stream frame too large: ${len} bytes`);
}
if (queued < 5 + len) break;
drop(5);
const raw = peek(len) ?? Buffer.alloc(0);
@@ -339,18 +382,15 @@ async function* streamChatEvents(args: {
if (flags & 0x01) payload = zlib.gunzipSync(raw);
if (flags & 0x02) {
sawEos = true;
const text = payload.toString("utf8");
if (text.includes('"error"')) {
try {
const parsed = JSON.parse(text) as { error?: { message?: string } };
trailerError = parsed.error?.message ?? text;
} catch {
trailerError = text;
}
}
trailerError = readTrailerError(payload.toString("utf8").trim());
continue;
}
yield* decodeChatFrame(payload);
for (const event of decodeChatFrame(payload)) {
if (event.kind === "text" || event.kind === "reasoning" || event.kind === "tool_call_start") {
producedOutput = true;
}
yield event;
}
}
}
} finally {
@@ -360,19 +400,35 @@ async function* streamChatEvents(args: {
// ignore
}
try {
await resp.body?.cancel();
void resp.body?.cancel();
} catch {
// ignore
}
}
if (trailerError) throw new Error(trailerError);
if (trailerError) {
if (
!producedOutput &&
trailerError.code.toLowerCase() === "invalid_argument" &&
/\binternal error\b/i.test(trailerError.message) &&
args.request.historyBytes >= LARGE_HISTORY_RECOVERY_BYTES
) {
throw new Error(
`Devin stream rejected: ${trailerError.message} (message history of ${args.request.historyBytes} bytes likely exceeds the context window)`,
);
}
throw new Error(
trailerError.code
? `Devin stream rejected (${trailerError.code}): ${trailerError.message}`
: `Devin stream rejected: ${trailerError.message}`,
);
}
if (!sawEos) throw new Error("Devin stream ended without an EOS trailer");
}
export function streamDevin(
model: Model<Api>,
context: TranscriptContext,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream {
const stream = createAssistantMessageEventStream();
@@ -398,10 +454,9 @@ export function streamDevin(
let textOpen = false;
let thinkingOpen = false;
let toolIndex = -1;
let partialJson = "";
let toolId = "";
let toolName = "";
const openTools = new Map<string, number>();
const toolJson = new Map<string, string>();
let lastToolId: string | undefined;
const closeText = () => {
if (!textOpen) return;
@@ -421,38 +476,92 @@ export function streamDevin(
}
thinkingOpen = false;
};
const closeTool = () => {
if (toolIndex < 0) return;
const block = output.content[toolIndex];
const closeTool = (id: string) => {
const idx = openTools.get(id);
if (idx === undefined) return;
openTools.delete(id);
const block = output.content[idx];
if (block.type === "toolCall") {
block.arguments = parseStreamingJson(partialJson);
const json = toolJson.get(id) ?? "";
try {
block.arguments = JSON.parse(json);
} catch {
// keep last parsed object
}
stream.push({
type: "toolcall_end",
contentIndex: toolIndex,
toolCall: { type: "toolCall", id: toolId, name: toolName, arguments: block.arguments },
contentIndex: idx,
toolCall: { type: "toolCall", id, name: block.name, arguments: block.arguments },
partial: output,
});
}
toolIndex = -1;
toolJson.delete(id);
if (lastToolId === id) lastToolId = undefined;
};
const closeAllTools = () => {
for (const id of [...openTools.keys()]) closeTool(id);
};
const currentThinkingBlock = () => {
const block = output.content[output.content.length - 1];
return block?.type === "thinking" ? block : undefined;
};
try {
const apiKey = options?.apiKey;
if (!apiKey) throw new Error("No Devin credentials. Run /login devin (uses the local Devin CLI).");
if (options?.deferred) throw new Error("The devin provider does not support deferred responses");
const host = (options?.env?.DEVIN_API_SERVER_URL || "https://server.codeium.com").replace(/\/$/, "");
const modelUid = resolveModelUid(model.id, model.thinkingLevelMap, options?.reasoning);
const mapped = mapContextToChat(context);
stream.push({ type: "start", partial: output });
output.providerThinkingLevel = modelUid;
output.responseModel = modelUid;
const mapped = mapContextToChat(context, model);
const timeoutMs = options?.timeoutMs;
const signal =
timeoutMs && timeoutMs > 0
? anySignal(
[options?.signal, AbortSignal.timeout(timeoutMs)].filter((s): s is AbortSignal => Boolean(s)),
)
: options?.signal;
const ids = sessionIds(apiKey, host, options);
const userJwt = await getCachedUserJwt(apiKey, host, signal);
for await (const event of streamChatEvents({
const sampling =
model.samplingParams || options?.samplingParams
? { ...model.samplingParams, ...options?.samplingParams }
: {};
const wireArgs = {
apiKey,
host,
userJwt,
modelUid,
systemPrompt: mapped.systemPrompt,
messages: mapped.messages,
tools: mapped.tools.length > 0 ? mapped.tools : undefined,
maxOutputTokens: options?.maxTokens,
signal: options?.signal,
cascadeId: ids.cascadeId,
promptId: randomUUID(),
sessionId: ids.sessionId,
requestId: BigInt(Date.now()),
triggerId: randomUUID(),
maxTokens: clampMaxTokensToContext(model, context, options?.maxTokens ?? model.maxTokens),
temperature: options?.temperature ?? 1.0,
topP: typeof sampling.topP === "number" ? sampling.topP : 0.95,
topK: typeof sampling.topK === "number" ? sampling.topK : 40,
};
const overridden = await options?.onPayload?.(wireArgs, model);
const finalArgs =
overridden && typeof overridden === "object" ? { ...wireArgs, ...overridden } : wireArgs;
const request = buildGetChatMessageRequest(finalArgs);
stream.push({ type: "start", partial: output });
for await (const event of streamChatEvents({
host,
request,
headers: options?.headers,
signal,
fetchFn: options?.fetch,
onResponse: options?.onResponse
? (response) => options?.onResponse?.(response, model)
: undefined,
})) {
if (event.kind === "text") {
closeThinking();
@@ -480,46 +589,66 @@ export function streamDevin(
block.thinking += event.text;
stream.push({ type: "thinking_delta", contentIndex: idx, delta: event.text, partial: output });
}
} else if (event.kind === "signature") {
const block = currentThinkingBlock();
if (block) block.thinkingSignature = event.text;
} else if (event.kind === "thinking_redacted") {
const block = currentThinkingBlock();
if (block) block.redacted = true;
} else if (event.kind === "message_id") {
if (!output.responseId) output.responseId = event.id;
} else if (event.kind === "actual_model") {
output.responseModel = event.uid;
} else if (event.kind === "tool_call_start") {
closeText();
closeThinking();
closeTool();
toolId = event.id;
toolName = event.name;
partialJson = "";
output.content.push({ type: "toolCall", id: event.id, name: event.name, arguments: {} });
toolIndex = output.content.length - 1;
stream.push({ type: "toolcall_start", contentIndex: toolIndex, partial: output });
openTools.set(event.id, output.content.length - 1);
toolJson.set(event.id, "");
lastToolId = event.id;
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
} else if (event.kind === "tool_call_args") {
if (toolIndex < 0) continue;
partialJson += event.argsDelta;
const block = output.content[toolIndex];
const id = event.id ?? lastToolId;
const idx = id === undefined ? undefined : openTools.get(id);
if (id === undefined || idx === undefined) continue;
const json = (toolJson.get(id) ?? "") + event.argsDelta;
toolJson.set(id, json);
const block = output.content[idx];
if (block.type === "toolCall") {
block.arguments = parseStreamingJson(partialJson);
try {
block.arguments = JSON.parse(json);
} catch {
// incomplete json
}
}
stream.push({ type: "toolcall_delta", contentIndex: toolIndex, delta: event.argsDelta, partial: output });
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: event.argsDelta, partial: output });
} else if (event.kind === "finish") {
closeText();
closeThinking();
closeTool();
closeAllTools();
if (event.reason === "content_filter") {
throw new Error("Devin stop_reason: content_filter");
}
if (event.reason === "error") {
throw new Error("Devin stop_reason: error");
}
output.stopReason =
event.reason === "tool_calls" ? "toolUse" : event.reason === "length" ? "length" : "stop";
} else if (event.kind === "usage") {
// Empty trailers must not erase usage already received. Metrics are
// snapshots, and a frame may contain only some of the components.
if (!event.totalTokens) continue;
output.usage.input = event.promptTokens ?? output.usage.input;
output.usage.output = event.completionTokens ?? output.usage.output;
output.usage.cacheRead = event.cachedInputTokens ?? output.usage.cacheRead;
output.usage.cacheWrite = event.cacheCreationInputTokens ?? output.usage.cacheWrite;
output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
output.usage.input = event.input ?? 0;
output.usage.output = event.output ?? 0;
output.usage.cacheRead = event.cacheRead ?? 0;
output.usage.cacheWrite = event.cacheWrite ?? 0;
output.usage.totalTokens =
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
if (!output.responseModel && event.modelUid) output.responseModel = event.modelUid;
calculateCost(model, output.usage);
}
}
closeText();
closeThinking();
closeTool();
closeAllTools();
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
stream.end();
} catch (error) {