@buyong/pi-devin 0.1.4 → 0.1.6
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/README.ko.md +222 -0
- package/README.md +37 -41
- package/index.ts +6 -12
- package/package.json +4 -1
- package/src/core/auth/access.ts +7 -3
- package/src/core/auth/callback-server.ts +57 -0
- package/src/core/auth/index.ts +2 -1
- package/src/core/auth/login.ts +104 -151
- package/src/core/auth/pkce.ts +32 -0
- package/src/core/auth/rest-exchange.ts +32 -30
- package/src/core/cancellation.ts +99 -0
- package/src/core/client.ts +127 -122
- package/src/core/errors.ts +115 -0
- package/src/core/index.ts +18 -4
- package/src/core/inference/accumulator.ts +174 -0
- package/src/core/inference/images.ts +22 -15
- package/src/core/inference/request.ts +59 -0
- package/src/core/inference/retry.ts +31 -31
- package/src/core/inference/state.ts +7 -3
- package/src/core/inference/stream-chat.ts +48 -0
- package/src/core/models/access.ts +4 -3
- package/src/core/models/catalog.ts +29 -20
- package/src/core/models/select.ts +14 -12
- package/src/core/models/variants.ts +85 -72
- package/src/core/protocol/connect-client.ts +288 -0
- package/src/core/protocol/device.ts +33 -34
- package/src/core/protocol/endpoint.ts +22 -0
- package/src/core/protocol/fingerprint.ts +38 -26
- package/src/core/protocol/frames.ts +101 -0
- package/src/core/protocol/http/fetch-client.ts +42 -0
- package/src/core/protocol/http/node-client.ts +130 -0
- package/src/core/protocol/http/response.ts +99 -0
- package/src/core/protocol/http.ts +41 -101
- package/src/core/protocol/protobuf.ts +93 -74
- package/src/core/redaction.ts +33 -0
- package/src/core/services/device-identity.ts +15 -0
- package/src/core/services/http-transport.ts +28 -0
- package/src/core/services/platform.ts +54 -0
- package/src/core/types.ts +2 -23
- package/src/core/validation.ts +25 -0
- package/src/pi/adapters/pi/devin-command.ts +82 -0
- package/src/pi/adapters/pi/fusion-command.ts +92 -0
- package/src/pi/adapters/pi/oauth.ts +14 -0
- package/src/pi/adapters/pi/provider.ts +22 -0
- package/src/pi/adapters/pi/registration.ts +89 -0
- package/src/pi/adapters/pi/response-stream.ts +164 -0
- package/src/pi/adapters/pi/sidekick-tool.ts +45 -0
- package/src/pi/adapters/pi/transcript.ts +177 -0
- package/src/pi/adapters/pi/types.ts +14 -0
- package/src/pi/{devin-panel.ts → adapters/pi/ui/devin-panel.ts} +11 -9
- package/src/pi/{fusion-picker.ts → adapters/pi/ui/fusion-picker.ts} +1 -1
- package/src/pi/adapters/pi/usage-status.ts +65 -0
- package/src/pi/adapters/storage/files.ts +66 -0
- package/src/pi/adapters/storage/json-file.ts +31 -0
- package/src/pi/app/config.ts +25 -0
- package/src/pi/app/environment.ts +30 -0
- package/src/pi/app/layer.ts +43 -0
- package/src/pi/app/runtime.ts +20 -0
- package/src/pi/app/session.ts +26 -0
- package/src/pi/features/accounts/service.ts +69 -0
- package/src/pi/features/accounts/types.ts +25 -0
- package/src/pi/features/auth/credential.ts +40 -0
- package/src/pi/features/auth/guard.ts +25 -0
- package/src/pi/features/auth/login.ts +116 -0
- package/src/pi/features/auth/state.ts +48 -0
- package/src/pi/features/auth/types.ts +36 -0
- package/src/pi/features/bounded.ts +4 -0
- package/src/pi/features/catalog/constants.ts +10 -0
- package/src/pi/features/catalog/fusion.ts +45 -0
- package/src/pi/features/catalog/ids.ts +79 -0
- package/src/pi/features/catalog/projection.ts +93 -0
- package/src/pi/features/catalog/remote.ts +15 -0
- package/src/pi/features/catalog/restore.ts +53 -0
- package/src/pi/features/catalog/service.ts +158 -0
- package/src/pi/features/catalog/types.ts +56 -0
- package/src/pi/features/clients/service.ts +47 -0
- package/src/pi/features/clients/types.ts +22 -0
- package/src/pi/features/default-scheduler.ts +12 -0
- package/src/pi/features/inference/request.ts +190 -0
- package/src/pi/features/inference/service.ts +33 -0
- package/src/pi/features/inference/types.ts +11 -0
- package/src/pi/features/lifetime/service.ts +19 -0
- package/src/pi/features/lifetime/types.ts +13 -0
- package/src/pi/features/runs/admission.ts +36 -0
- package/src/pi/features/runs/lifecycle.ts +207 -0
- package/src/pi/features/runs/service.ts +10 -0
- package/src/pi/features/runs/state.ts +37 -0
- package/src/pi/features/runs/types.ts +43 -0
- package/src/pi/features/settings/service.ts +46 -0
- package/src/pi/features/settings/types.ts +36 -0
- package/src/pi/features/sidekick/child.ts +228 -0
- package/src/pi/features/sidekick/command.ts +80 -0
- package/src/pi/features/sidekick/launcher.ts +12 -0
- package/src/pi/features/sidekick/prompts.ts +17 -0
- package/src/pi/features/sidekick/report.ts +51 -0
- package/src/pi/features/sidekick/service.ts +176 -0
- package/src/pi/features/sidekick/types.ts +99 -0
- package/src/pi/features/usage/changes.ts +20 -0
- package/src/pi/{usage.ts → features/usage/report.ts} +3 -34
- package/src/pi/features/usage/types.ts +33 -0
- package/src/core/inference/chat.ts +0 -234
- package/src/core/protocol/transport.ts +0 -513
- package/src/pi/auth.ts +0 -145
- package/src/pi/devin-command.ts +0 -76
- package/src/pi/devin-settings.ts +0 -50
- package/src/pi/extension.ts +0 -80
- package/src/pi/fusion-command.ts +0 -83
- package/src/pi/fusion-settings.ts +0 -44
- package/src/pi/lifecycle.ts +0 -217
- package/src/pi/messages.ts +0 -131
- package/src/pi/models.ts +0 -379
- package/src/pi/provider.ts +0 -249
- package/src/pi/sidekick-report.ts +0 -47
- package/src/pi/sidekick-runtime.ts +0 -300
- package/src/pi/sidekick.ts +0 -211
- package/src/pi/stream.ts +0 -124
package/src/pi/sidekick.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { DEVIN_API, type DevinModel } from "./models.ts";
|
|
5
|
-
import type { DevinProvider } from "./provider.ts";
|
|
6
|
-
import { boundReport, type BoundedReport } from "./sidekick-report.ts";
|
|
7
|
-
import { SidekickRuntime, type Handoff, type HandoffReport, type SidekickLaunch, type SidekickRuntimeOptions } from "./sidekick-runtime.ts";
|
|
8
|
-
|
|
9
|
-
export const SIDEKICK_TOOL_NAME = "devin_sidekick";
|
|
10
|
-
/** Marker set on the child Pi started as the sidekick. The child's extension does not register Fusion again. */
|
|
11
|
-
export const FUSION_CHILD_ENV = "PI_DEVIN_FUSION_CHILD";
|
|
12
|
-
/** Custom message type that delivers a background handoff report to the lead. */
|
|
13
|
-
export const SIDEKICK_REPORT_MESSAGE = "devin-sidekick-report";
|
|
14
|
-
/** Appended by the provider to Fusion lead requests only, so a custom system prompt cannot drop it. */
|
|
15
|
-
export const FUSION_LEAD_GUIDANCE = [
|
|
16
|
-
`Fusion mode: you are the lead. Plan, talk to the user, and decide; delegate hands-on exploration, implementation, and verification to the \`${SIDEKICK_TOOL_NAME}\` tool with clear context and done criteria.`,
|
|
17
|
-
`The sidekick is a separate agent in this workspace with the same tools, and it keeps its context across handoffs. \`${SIDEKICK_TOOL_NAME}\` waits for the report by default; pass \`block: false\` to keep working while it runs, and its report arrives later as a message. Calling \`${SIDEKICK_TOOL_NAME}\` while it is busy adds instructions to the running handoff.`,
|
|
18
|
-
"Review its report critically, decide the next step, and do not redo work it already reported. Present the combined result as your own work.",
|
|
19
|
-
].join("\n");
|
|
20
|
-
const SYSTEM_PROMPT = [
|
|
21
|
-
"You are the sidekick: a hands-on software engineer paired with a lead agent in the same workspace.",
|
|
22
|
-
"The lead sends you briefs with context and done criteria and may add instructions while you work. Do the work with the available tools: explore, implement, and verify.",
|
|
23
|
-
"You cannot talk to the user. When you finish, reply without tool calls with a concise report: what you did, how you verified it, and anything unresolved or risky.",
|
|
24
|
-
"Your conversation persists across briefs from the same lead session, so build on what you already know.",
|
|
25
|
-
].join("\n");
|
|
26
|
-
|
|
27
|
-
export interface SidekickDetails {
|
|
28
|
-
sidekickModel: string;
|
|
29
|
-
handoffId: string;
|
|
30
|
-
status: HandoffReport["status"] | "running" | "background";
|
|
31
|
-
toolCalls: number;
|
|
32
|
-
durationMs?: number;
|
|
33
|
-
isSteer: boolean;
|
|
34
|
-
/** Complete report saved when the text the lead received was cut. */
|
|
35
|
-
fullReportPath?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface SidekickToolOptions {
|
|
39
|
-
/** Pi agent directory that holds sidekick session files. */
|
|
40
|
-
agentDir: () => string;
|
|
41
|
-
/** Lets tests replace how the child is started. */
|
|
42
|
-
runtime?: Omit<SidekickRuntimeOptions, "childEnv" | "onProgress">;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The host may copy model objects in its settings layer, so Fusion data is looked up by id in the provider's current list. */
|
|
46
|
-
export function fusionOf(provider: Pick<DevinProvider, "getModels">, model: Model<any> | undefined): NonNullable<DevinModel["devin"]["fusion"]> | undefined {
|
|
47
|
-
if (model?.provider !== "devin" || model.api !== DEVIN_API) return undefined;
|
|
48
|
-
return (provider.getModels() as readonly DevinModel[]).find((candidate) => candidate.id === model.id)?.devin.fusion;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const launchKey = (launch: SidekickLaunch) => JSON.stringify([launch.cwd, launch.modelID, launch.thinking, launch.sessionFile, launch.tools]);
|
|
52
|
-
const shortID = (id: string) => id.slice(0, 8);
|
|
53
|
-
|
|
54
|
-
/** Bound and preserve the complete report, including status, errors and any background delivery heading. */
|
|
55
|
-
async function reportText(report: HandoffReport, heading = ""): Promise<BoundedReport> {
|
|
56
|
-
const body = report.text.trim();
|
|
57
|
-
const reason = report.error ?? (report.status === "aborted" ? "cancelled." : "failed.");
|
|
58
|
-
const text = report.status === "completed" ? body || "(The sidekick finished without a report.)"
|
|
59
|
-
: [`The sidekick task ${report.status === "aborted" ? "stopped" : "failed"}: ${reason}`, body].filter(Boolean).join("\n\n");
|
|
60
|
-
return boundReport(heading + text);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface ReportDelivery {
|
|
64
|
-
owner: "background" | "blocking" | "delivered";
|
|
65
|
-
scheduled: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const reportPath = (fullReportPath: string | undefined) => fullReportPath ? { fullReportPath } : {};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The lead's `devin_sidekick` tool. The sidekick runs as one child Pi process per lead session and uses the lead's active tools.
|
|
72
|
-
* By default it waits for the report; with `block: false`, or when a user message arrives while waiting, it moves to the background and delivers the report once.
|
|
73
|
-
*/
|
|
74
|
-
export function createSidekickTool(pi: ExtensionAPI, provider: DevinProvider, options: SidekickToolOptions) {
|
|
75
|
-
const runtimes = new Map<string, SidekickRuntime>();
|
|
76
|
-
// Steers share the same done promise; completed handoffs must not be retained by delivery bookkeeping.
|
|
77
|
-
const deliveries = new WeakMap<Promise<HandoffReport>, ReportDelivery>();
|
|
78
|
-
const waits = new Set<() => void>();
|
|
79
|
-
let progressListener: (() => void) | undefined;
|
|
80
|
-
|
|
81
|
-
const deliverLater = (handoff: Handoff, runtime: SidekickRuntime, delivery: ReportDelivery) => {
|
|
82
|
-
if (delivery.scheduled || delivery.owner === "delivered") return;
|
|
83
|
-
delivery.scheduled = true;
|
|
84
|
-
void handoff.done.then(async (report) => {
|
|
85
|
-
try {
|
|
86
|
-
// A blocking call owns the report unless user input moves it back to the background.
|
|
87
|
-
const canDeliver = () => delivery.owner === "background" && [...runtimes.values()].includes(runtime);
|
|
88
|
-
if (report.status === "aborted" || !canDeliver()) return;
|
|
89
|
-
const { text, fullReportPath } = await reportText(report, `Sidekick report for background handoff ${shortID(report.id)}:\n\n`);
|
|
90
|
-
if (!canDeliver()) return;
|
|
91
|
-
delivery.owner = "delivered";
|
|
92
|
-
pi.sendMessage({
|
|
93
|
-
customType: SIDEKICK_REPORT_MESSAGE, display: true, content: text,
|
|
94
|
-
details: { sidekickModel: runtime.launch.modelID, handoffId: report.id, status: report.status, toolCalls: report.toolCalls, durationMs: report.durationMs, isSteer: false,
|
|
95
|
-
...reportPath(fullReportPath) } satisfies SidekickDetails,
|
|
96
|
-
}, { deliverAs: "followUp", triggerTurn: true });
|
|
97
|
-
} finally { delivery.scheduled = false; }
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const definition: ToolDefinition = {
|
|
102
|
-
name: SIDEKICK_TOOL_NAME,
|
|
103
|
-
label: "Sidekick",
|
|
104
|
-
description: "Delegate hands-on work to your persistent sidekick, a separate coding agent that shares this workspace and your tools. "
|
|
105
|
-
+ "Put the task context and done criteria in `message`. By default the call waits and returns the sidekick's report; "
|
|
106
|
-
+ "set `block` to false to continue while it works, and the report arrives later as a message. "
|
|
107
|
-
+ "Calling it while the sidekick is busy adds instructions to the running handoff.",
|
|
108
|
-
promptSnippet: "Delegate hands-on work to a persistent sidekick agent and get its report",
|
|
109
|
-
parameters: {
|
|
110
|
-
type: "object",
|
|
111
|
-
properties: {
|
|
112
|
-
message: { type: "string", description: "Brief for the sidekick: context, the task, and done criteria." },
|
|
113
|
-
block: { type: "boolean", description: "Wait for the report (default true). Use false to keep working while the sidekick runs." },
|
|
114
|
-
},
|
|
115
|
-
required: ["message"],
|
|
116
|
-
additionalProperties: false,
|
|
117
|
-
} as unknown as ToolDefinition["parameters"],
|
|
118
|
-
defaultActive: false,
|
|
119
|
-
executionMode: "sequential",
|
|
120
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
121
|
-
signal?.throwIfAborted();
|
|
122
|
-
const fusion = fusionOf(provider, ctx.model);
|
|
123
|
-
if (!fusion) throw new Error("The sidekick is only available with the Devin Fusion model.");
|
|
124
|
-
const input = params as { message?: unknown; block?: unknown } | undefined;
|
|
125
|
-
const message = typeof input?.message === "string" ? input.message.trim() : "";
|
|
126
|
-
if (!message) throw new Error("A message for the sidekick is required.");
|
|
127
|
-
// The lead list hides exact UIDs covered by groups, so the level comes from the Fusion candidates.
|
|
128
|
-
const sidekickChoice = provider.getFusionChoices()?.sidekicks.find((item) => item.id === fusion.sidekickID);
|
|
129
|
-
if (!sidekickChoice) throw new Error("The sidekick model is not in the current Devin model list. Refresh the model list.");
|
|
130
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
131
|
-
const launch: SidekickLaunch = {
|
|
132
|
-
cwd: ctx.cwd, modelID: fusion.sidekickID, thinking: sidekickChoice.level,
|
|
133
|
-
sessionFile: ctx.sessionManager.getSessionFile() ? join(options.agentDir(), "pi-devin", "sidekick", `${sessionId}.jsonl`) : undefined,
|
|
134
|
-
tools: pi.getActiveTools().filter((name) => name !== SIDEKICK_TOOL_NAME),
|
|
135
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
136
|
-
};
|
|
137
|
-
let runtime = runtimes.get(sessionId);
|
|
138
|
-
// When the pair or tools change, only an idle process is restarted. Context continues through the same session file.
|
|
139
|
-
if (runtime && !runtime.isBusy() && launchKey(runtime.launch) !== launchKey(launch)) { runtime.close(); runtime = undefined; }
|
|
140
|
-
if (!runtime) {
|
|
141
|
-
runtime = new SidekickRuntime(launch, { ...options.runtime, childEnv: { [FUSION_CHILD_ENV]: "1" }, onProgress: () => progressListener?.() });
|
|
142
|
-
runtimes.set(sessionId, runtime);
|
|
143
|
-
}
|
|
144
|
-
signal?.throwIfAborted();
|
|
145
|
-
const handoff = runtime.handoff(message);
|
|
146
|
-
let delivery = deliveries.get(handoff.done);
|
|
147
|
-
if (!delivery) {
|
|
148
|
-
delivery = { owner: "background", scheduled: false };
|
|
149
|
-
deliveries.set(handoff.done, delivery);
|
|
150
|
-
}
|
|
151
|
-
const details = (status: SidekickDetails["status"], report?: HandoffReport): SidekickDetails => ({
|
|
152
|
-
sidekickModel: launch.modelID, handoffId: handoff.id, status, toolCalls: report?.toolCalls ?? runtime!.progress()?.toolCalls ?? 0,
|
|
153
|
-
...(report ? { durationMs: report.durationMs } : {}), isSteer: handoff.isSteer,
|
|
154
|
-
});
|
|
155
|
-
const backgroundResult = (reason: string) => {
|
|
156
|
-
deliverLater(handoff, runtime!, delivery);
|
|
157
|
-
return { content: [{ type: "text" as const, text: `${reason} The report will arrive as a message. (handoff ${shortID(handoff.id)})` }], details: details("background") };
|
|
158
|
-
};
|
|
159
|
-
if (input?.block === false) {
|
|
160
|
-
return backgroundResult(handoff.isSteer ? "Added instructions to the running sidekick task." : "The sidekick started working in the background.");
|
|
161
|
-
}
|
|
162
|
-
delivery.owner = "blocking";
|
|
163
|
-
let settle!: (outcome: "done" | "aborted" | "interrupted") => void;
|
|
164
|
-
const waiting = new Promise<"done" | "aborted" | "interrupted">((resolve) => { settle = resolve; });
|
|
165
|
-
const interrupt = () => settle("interrupted"), abort = () => settle("aborted");
|
|
166
|
-
waits.add(interrupt);
|
|
167
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
168
|
-
progressListener = () => {
|
|
169
|
-
const progress = runtime!.progress();
|
|
170
|
-
if (progress) onUpdate?.({ content: [{ type: "text", text: `sidekick: ${progress.toolCalls} tool calls${progress.recentTools.length ? ` · ${progress.recentTools.join(", ")}` : ""}` }], details: details("running") });
|
|
171
|
-
};
|
|
172
|
-
if (signal?.aborted) abort();
|
|
173
|
-
void handoff.done.then(() => settle("done"));
|
|
174
|
-
const outcome = await waiting.finally(() => {
|
|
175
|
-
progressListener = undefined;
|
|
176
|
-
waits.delete(interrupt);
|
|
177
|
-
signal?.removeEventListener("abort", abort);
|
|
178
|
-
});
|
|
179
|
-
if (outcome === "interrupted") {
|
|
180
|
-
delivery.owner = "background";
|
|
181
|
-
return backgroundResult("A user message arrived, so the sidekick task moved to the background.");
|
|
182
|
-
}
|
|
183
|
-
delivery.owner = "delivered";
|
|
184
|
-
if (outcome === "aborted") {
|
|
185
|
-
runtime.abort();
|
|
186
|
-
throw new Error("Cancelled the sidekick task.");
|
|
187
|
-
}
|
|
188
|
-
const report = await handoff.done;
|
|
189
|
-
const { text, fullReportPath } = await reportText(report);
|
|
190
|
-
if (report.status !== "completed") throw new Error(text);
|
|
191
|
-
return { content: [{ type: "text" as const, text }], details: { ...details(report.status, report), ...reportPath(fullReportPath) } };
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
definition,
|
|
197
|
-
/** On user input, a waiting handoff moves to the background. Input sent by extensions does not count. */
|
|
198
|
-
onInput(source: string): void {
|
|
199
|
-
if (source === "extension") return;
|
|
200
|
-
for (const interrupt of [...waits]) interrupt();
|
|
201
|
-
},
|
|
202
|
-
/** Cleans up the sidekick of a different lead session. */
|
|
203
|
-
onSessionStart(sessionId: string): void {
|
|
204
|
-
for (const [id, runtime] of runtimes) if (id !== sessionId) { runtime.close(); runtimes.delete(id); }
|
|
205
|
-
},
|
|
206
|
-
reset(): void {
|
|
207
|
-
for (const runtime of runtimes.values()) runtime.close();
|
|
208
|
-
runtimes.clear();
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
package/src/pi/stream.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { createAssistantMessageEventStream, type AssistantMessage, type Model, type ToolCall } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ChatAccumulator } from "../core/inference/chat.ts";
|
|
3
|
-
import type { ChatMessage, ChatResult, JsonObject } from "../core/types.ts";
|
|
4
|
-
import { attachReplayMetadata } from "./messages.ts";
|
|
5
|
-
import { DEVIN_API } from "./models.ts";
|
|
6
|
-
|
|
7
|
-
export class DevinResponseStream {
|
|
8
|
-
readonly stream = createAssistantMessageEventStream();
|
|
9
|
-
readonly message: AssistantMessage;
|
|
10
|
-
private active?: { type: "text" | "thinking"; index: number };
|
|
11
|
-
private tools = new Map<string, { index: number; characters: number; hasEnded: boolean }>();
|
|
12
|
-
private hasTerminated = false;
|
|
13
|
-
private usageModelUid?: string;
|
|
14
|
-
private hasUsageReported = false;
|
|
15
|
-
constructor(model: Model<typeof DEVIN_API>, requestedUID: string) {
|
|
16
|
-
this.message = {
|
|
17
|
-
role: "assistant", api: DEVIN_API, provider: "devin", model: model.id, providerThinkingLevel: requestedUID,
|
|
18
|
-
content: [], stopReason: "pending", timestamp: Date.now(),
|
|
19
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
20
|
-
};
|
|
21
|
-
this.stream.push({ type: "start", partial: this.message });
|
|
22
|
-
}
|
|
23
|
-
private closeText(): void {
|
|
24
|
-
if (!this.active) return;
|
|
25
|
-
const block = this.message.content[this.active.index];
|
|
26
|
-
if (block.type === "text") this.stream.push({ type: "text_end", contentIndex: this.active.index, content: block.text, partial: this.message });
|
|
27
|
-
else if (block.type === "thinking") this.stream.push({ type: "thinking_end", contentIndex: this.active.index, content: block.thinking, partial: this.message });
|
|
28
|
-
this.active = undefined;
|
|
29
|
-
}
|
|
30
|
-
private text(type: "text" | "thinking", delta: string, redacted = false): void {
|
|
31
|
-
if (!this.active || this.active.type !== type) {
|
|
32
|
-
this.closeText();
|
|
33
|
-
const index = this.message.content.length;
|
|
34
|
-
this.message.content.push(type === "text" ? { type, text: "" } : { type, thinking: "", ...(redacted ? { redacted: true } : {}) });
|
|
35
|
-
this.active = { type, index };
|
|
36
|
-
this.stream.push({ type: type === "text" ? "text_start" : "thinking_start", contentIndex: index, partial: this.message });
|
|
37
|
-
}
|
|
38
|
-
const block = this.message.content[this.active.index];
|
|
39
|
-
if (!delta) return;
|
|
40
|
-
if (block.type === "text") {
|
|
41
|
-
block.text += delta;
|
|
42
|
-
this.stream.push({ type: "text_delta", contentIndex: this.active.index, delta, partial: this.message });
|
|
43
|
-
} else if (block.type === "thinking") {
|
|
44
|
-
block.thinking += delta;
|
|
45
|
-
this.stream.push({ type: "thinking_delta", contentIndex: this.active.index, delta, partial: this.message });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
accept(data: JsonObject, accumulator: ChatAccumulator): void {
|
|
49
|
-
if (this.hasTerminated) throw new Error("Cannot add a delta to a finished Pi response.");
|
|
50
|
-
if (accumulator.message.thinkingRedacted) {
|
|
51
|
-
for (const block of this.message.content) if (block.type === "thinking") { block.thinking = ""; block.redacted = true; }
|
|
52
|
-
if (!this.message.content.some((block) => block.type === "thinking")) this.text("thinking", "", true);
|
|
53
|
-
} else if (typeof data.deltaThinking === "string" && data.deltaThinking) this.text("thinking", data.deltaThinking);
|
|
54
|
-
if (typeof data.deltaText === "string" && data.deltaText) this.text("text", data.deltaText);
|
|
55
|
-
for (const call of accumulator.calls.values()) {
|
|
56
|
-
let state = this.tools.get(call.id);
|
|
57
|
-
if (!state) {
|
|
58
|
-
this.closeText();
|
|
59
|
-
const index = this.message.content.length;
|
|
60
|
-
this.message.content.push({ type: "toolCall", id: call.id, name: "", arguments: {} });
|
|
61
|
-
state = { index, characters: 0, hasEnded: false }; this.tools.set(call.id, state);
|
|
62
|
-
this.stream.push({ type: "toolcall_start", contentIndex: index, partial: this.message });
|
|
63
|
-
}
|
|
64
|
-
const block = this.message.content[state.index] as ToolCall;
|
|
65
|
-
block.name = call.name;
|
|
66
|
-
try { const value = JSON.parse(call.argumentsJson); if (value && typeof value === "object" && !Array.isArray(value)) block.arguments = value; } catch {}
|
|
67
|
-
const delta = call.argumentsJson.slice(state.characters);
|
|
68
|
-
state.characters = call.argumentsJson.length;
|
|
69
|
-
if (delta) this.stream.push({ type: "toolcall_delta", contentIndex: state.index, delta, partial: this.message });
|
|
70
|
-
}
|
|
71
|
-
if (accumulator.message.messageId) this.message.responseId = accumulator.message.messageId;
|
|
72
|
-
if (typeof data.actualModelUid === "string") this.message.responseModel = data.actualModelUid;
|
|
73
|
-
if (data.usage && typeof data.usage === "object") {
|
|
74
|
-
const usage = data.usage as JsonObject;
|
|
75
|
-
if (typeof usage.modelUid === "string") this.usageModelUid = usage.modelUid;
|
|
76
|
-
if (["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"].some((key) => usage[key] !== undefined)) this.hasUsageReported = true;
|
|
77
|
-
}
|
|
78
|
-
this.message.usage = { ...accumulator.usage, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
79
|
-
}
|
|
80
|
-
private closeContent(): void {
|
|
81
|
-
this.closeText();
|
|
82
|
-
for (const state of this.tools.values()) if (!state.hasEnded) {
|
|
83
|
-
state.hasEnded = true;
|
|
84
|
-
this.stream.push({ type: "toolcall_end", contentIndex: state.index, toolCall: this.message.content[state.index] as ToolCall, partial: this.message });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
private provenance(native: ChatMessage, usageReported: boolean): void {
|
|
88
|
-
this.message.providerThinkingLevel = native.modelUid ?? this.message.providerThinkingLevel;
|
|
89
|
-
attachReplayMetadata(this.message, native);
|
|
90
|
-
this.message.diagnostics = [{ type: "devin_provenance", timestamp: Date.now(), details: {
|
|
91
|
-
requestedUID: this.message.providerThinkingLevel ?? "", ...(this.message.responseModel ? { responseUID: this.message.responseModel } : {}),
|
|
92
|
-
...(this.usageModelUid ? { usageUID: this.usageModelUid } : {}), costsUnknown: true, usageReported,
|
|
93
|
-
} }];
|
|
94
|
-
}
|
|
95
|
-
redact(): void {
|
|
96
|
-
for (const block of this.message.content) {
|
|
97
|
-
if (block.type === "text") block.text = "";
|
|
98
|
-
else if (block.type === "thinking") { block.thinking = ""; block.redacted = true; }
|
|
99
|
-
else block.arguments = {};
|
|
100
|
-
}
|
|
101
|
-
this.closeContent(); this.message.content = []; this.tools.clear();
|
|
102
|
-
}
|
|
103
|
-
finish(result: ChatResult): void {
|
|
104
|
-
if (this.hasTerminated) return;
|
|
105
|
-
if (!this.message.content.length && (result.message.signature || result.message.geminiThoughtSignature || result.message.outputId)) this.text("text", "");
|
|
106
|
-
this.provenance(result.message, result.usageReported);
|
|
107
|
-
this.closeContent();
|
|
108
|
-
this.hasTerminated = true;
|
|
109
|
-
this.message.rawStopReason = result.stopReason;
|
|
110
|
-
this.message.stopReason = result.message.toolCalls?.length ? "toolUse" : result.stopReason === "STOP_REASON_MAX_TOKENS" ? "length" : "stop";
|
|
111
|
-
this.stream.push({ type: "done", reason: this.message.stopReason, message: this.message });
|
|
112
|
-
this.stream.end(this.message);
|
|
113
|
-
}
|
|
114
|
-
fail(errorMessage: string, isAborted: boolean, native?: ChatMessage): void {
|
|
115
|
-
if (this.hasTerminated) return;
|
|
116
|
-
if (native) this.provenance(native, this.hasUsageReported);
|
|
117
|
-
this.closeContent();
|
|
118
|
-
this.hasTerminated = true;
|
|
119
|
-
this.message.stopReason = isAborted ? "aborted" : "error";
|
|
120
|
-
this.message.errorMessage = errorMessage;
|
|
121
|
-
this.stream.push({ type: "error", reason: this.message.stopReason, error: this.message });
|
|
122
|
-
this.stream.end(this.message);
|
|
123
|
-
}
|
|
124
|
-
}
|