@buyong/pi-devin 0.1.5 → 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.
Files changed (117) hide show
  1. package/README.ko.md +3 -5
  2. package/README.md +3 -5
  3. package/index.ts +6 -15
  4. package/package.json +3 -1
  5. package/src/core/auth/access.ts +7 -3
  6. package/src/core/auth/callback-server.ts +57 -0
  7. package/src/core/auth/index.ts +2 -1
  8. package/src/core/auth/login.ts +104 -151
  9. package/src/core/auth/pkce.ts +32 -0
  10. package/src/core/auth/rest-exchange.ts +32 -30
  11. package/src/core/cancellation.ts +99 -0
  12. package/src/core/client.ts +127 -122
  13. package/src/core/errors.ts +115 -0
  14. package/src/core/index.ts +18 -4
  15. package/src/core/inference/accumulator.ts +174 -0
  16. package/src/core/inference/images.ts +22 -15
  17. package/src/core/inference/request.ts +59 -0
  18. package/src/core/inference/retry.ts +31 -31
  19. package/src/core/inference/state.ts +7 -3
  20. package/src/core/inference/stream-chat.ts +48 -0
  21. package/src/core/models/access.ts +4 -3
  22. package/src/core/models/catalog.ts +29 -20
  23. package/src/core/models/select.ts +14 -12
  24. package/src/core/models/variants.ts +85 -72
  25. package/src/core/protocol/connect-client.ts +288 -0
  26. package/src/core/protocol/device.ts +33 -34
  27. package/src/core/protocol/endpoint.ts +22 -0
  28. package/src/core/protocol/fingerprint.ts +38 -26
  29. package/src/core/protocol/frames.ts +101 -0
  30. package/src/core/protocol/http/fetch-client.ts +42 -0
  31. package/src/core/protocol/http/node-client.ts +130 -0
  32. package/src/core/protocol/http/response.ts +99 -0
  33. package/src/core/protocol/http.ts +41 -101
  34. package/src/core/protocol/protobuf.ts +93 -74
  35. package/src/core/redaction.ts +33 -0
  36. package/src/core/services/device-identity.ts +15 -0
  37. package/src/core/services/http-transport.ts +28 -0
  38. package/src/core/services/platform.ts +54 -0
  39. package/src/core/types.ts +2 -23
  40. package/src/core/validation.ts +25 -0
  41. package/src/pi/adapters/pi/devin-command.ts +82 -0
  42. package/src/pi/adapters/pi/fusion-command.ts +92 -0
  43. package/src/pi/adapters/pi/oauth.ts +14 -0
  44. package/src/pi/adapters/pi/provider.ts +22 -0
  45. package/src/pi/adapters/pi/registration.ts +89 -0
  46. package/src/pi/adapters/pi/response-stream.ts +164 -0
  47. package/src/pi/adapters/pi/sidekick-tool.ts +45 -0
  48. package/src/pi/adapters/pi/transcript.ts +177 -0
  49. package/src/pi/adapters/pi/types.ts +14 -0
  50. package/src/pi/{devin-panel.ts → adapters/pi/ui/devin-panel.ts} +11 -9
  51. package/src/pi/{fusion-picker.ts → adapters/pi/ui/fusion-picker.ts} +1 -1
  52. package/src/pi/adapters/pi/usage-status.ts +65 -0
  53. package/src/pi/adapters/storage/files.ts +66 -0
  54. package/src/pi/adapters/storage/json-file.ts +31 -0
  55. package/src/pi/app/config.ts +25 -0
  56. package/src/pi/app/environment.ts +30 -0
  57. package/src/pi/app/layer.ts +43 -0
  58. package/src/pi/app/runtime.ts +20 -0
  59. package/src/pi/app/session.ts +26 -0
  60. package/src/pi/features/accounts/service.ts +69 -0
  61. package/src/pi/features/accounts/types.ts +25 -0
  62. package/src/pi/features/auth/credential.ts +40 -0
  63. package/src/pi/features/auth/guard.ts +25 -0
  64. package/src/pi/features/auth/login.ts +116 -0
  65. package/src/pi/features/auth/state.ts +48 -0
  66. package/src/pi/features/auth/types.ts +36 -0
  67. package/src/pi/features/bounded.ts +4 -0
  68. package/src/pi/features/catalog/constants.ts +10 -0
  69. package/src/pi/features/catalog/fusion.ts +45 -0
  70. package/src/pi/features/catalog/ids.ts +79 -0
  71. package/src/pi/features/catalog/projection.ts +93 -0
  72. package/src/pi/features/catalog/remote.ts +15 -0
  73. package/src/pi/features/catalog/restore.ts +53 -0
  74. package/src/pi/features/catalog/service.ts +158 -0
  75. package/src/pi/features/catalog/types.ts +56 -0
  76. package/src/pi/features/clients/service.ts +47 -0
  77. package/src/pi/features/clients/types.ts +22 -0
  78. package/src/pi/features/default-scheduler.ts +12 -0
  79. package/src/pi/features/inference/request.ts +190 -0
  80. package/src/pi/features/inference/service.ts +33 -0
  81. package/src/pi/features/inference/types.ts +11 -0
  82. package/src/pi/features/lifetime/service.ts +19 -0
  83. package/src/pi/features/lifetime/types.ts +13 -0
  84. package/src/pi/features/runs/admission.ts +36 -0
  85. package/src/pi/features/runs/lifecycle.ts +207 -0
  86. package/src/pi/features/runs/service.ts +10 -0
  87. package/src/pi/features/runs/state.ts +37 -0
  88. package/src/pi/features/runs/types.ts +43 -0
  89. package/src/pi/features/settings/service.ts +46 -0
  90. package/src/pi/features/settings/types.ts +36 -0
  91. package/src/pi/features/sidekick/child.ts +228 -0
  92. package/src/pi/features/sidekick/command.ts +80 -0
  93. package/src/pi/features/sidekick/launcher.ts +12 -0
  94. package/src/pi/features/sidekick/prompts.ts +17 -0
  95. package/src/pi/features/sidekick/report.ts +51 -0
  96. package/src/pi/features/sidekick/service.ts +176 -0
  97. package/src/pi/features/sidekick/types.ts +99 -0
  98. package/src/pi/features/usage/changes.ts +20 -0
  99. package/src/pi/{usage.ts → features/usage/report.ts} +3 -34
  100. package/src/pi/features/usage/types.ts +33 -0
  101. package/src/core/inference/chat.ts +0 -234
  102. package/src/core/protocol/transport.ts +0 -513
  103. package/src/pi/auth.ts +0 -166
  104. package/src/pi/devin-command.ts +0 -86
  105. package/src/pi/devin-settings.ts +0 -50
  106. package/src/pi/extension.ts +0 -89
  107. package/src/pi/fusion-command.ts +0 -83
  108. package/src/pi/fusion-settings.ts +0 -44
  109. package/src/pi/lifecycle.ts +0 -217
  110. package/src/pi/messages.ts +0 -131
  111. package/src/pi/models.ts +0 -415
  112. package/src/pi/provider.ts +0 -278
  113. package/src/pi/sidekick-report.ts +0 -47
  114. package/src/pi/sidekick-runtime.ts +0 -300
  115. package/src/pi/sidekick.ts +0 -211
  116. package/src/pi/stream.ts +0 -124
  117. package/src/pi/usage-store.ts +0 -75
@@ -1,217 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { awaitWithSignal } from "../core/protocol/transport.ts";
3
-
4
- export const DEVIN_STATE_ENTRY = "pi-devin.state.v1";
5
- export type ExecutionKind = "agent" | "pending" | "manual" | "tree" | "direct" | "recovered";
6
- export interface DevinSessionState {
7
- version: 1;
8
- sessionId: string;
9
- ownerHash?: string;
10
- cascadeId: string;
11
- trajectoryId: string;
12
- nextStepIndex: number;
13
- execution?: { id: string; used: number; settled: boolean; kind: ExecutionKind };
14
- }
15
- export interface SessionBinding {
16
- getSessionId(): string;
17
- getBranch(): readonly { type: string; customType?: string; data?: unknown }[];
18
- append(data: DevinSessionState): void;
19
- }
20
- interface Execution {
21
- data: NonNullable<DevinSessionState["execution"]>;
22
- controller: AbortController;
23
- tail: Promise<void>;
24
- cleanups: Set<() => void>;
25
- activeRequests: number;
26
- hasStarted: boolean;
27
- nextIsRecovery: boolean;
28
- hasCheckpoint: boolean;
29
- }
30
- export interface RequestLease {
31
- signal: AbortSignal;
32
- executionId: string;
33
- cascadeId: string;
34
- trajectoryId: string;
35
- sessionId: string;
36
- stepIndex: number;
37
- isAuxiliary: boolean;
38
- isRecovery: boolean;
39
- consume(): void;
40
- addCleanup(cleanup: () => void): void;
41
- release(): void;
42
- }
43
- export class ModelBudgetError extends Error {
44
- constructor(limit: number) { super(`Used all ${limit} Devin model HTTP requests in the budget.`); this.name = "ModelBudgetError"; }
45
- }
46
-
47
- /** Value of `PI_DEVIN_MAX_MODEL_CALLS`. Defaults to 8; anything other than an integer from 1 to 100 stops the extension from loading. */
48
- export function modelCallLimit(value: string | undefined): number | undefined {
49
- if (value === undefined || value === "") return undefined;
50
- if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 100) throw new Error("PI_DEVIN_MAX_MODEL_CALLS must be an integer from 1 to 100.");
51
- return Number(value);
52
- }
53
- const cancelled = () => new DOMException("The previous Devin run ended.", "AbortError");
54
- const isUUID = (value: unknown): value is string => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
55
-
56
- function restoreState(binding: SessionBinding): DevinSessionState {
57
- const sessionId = binding.getSessionId();
58
- const entry = [...binding.getBranch()].reverse().find((entry) => entry.type === "custom" && entry.customType === DEVIN_STATE_ENTRY);
59
- if (!entry) return { version: 1, sessionId, cascadeId: randomUUID(), trajectoryId: randomUUID(), nextStepIndex: 0 };
60
- const value = entry.data as DevinSessionState | undefined;
61
- if (!value || value.version !== 1 || typeof value.sessionId !== "string" || !isUUID(value.cascadeId) || !isUUID(value.trajectoryId)
62
- || !Number.isSafeInteger(value.nextStepIndex) || value.nextStepIndex < 0 || value.nextStepIndex > 2_147_483_647
63
- || (value.ownerHash !== undefined && !/^[a-f0-9]{64}$/.test(value.ownerHash))
64
- || (value.execution !== undefined && (!isUUID(value.execution.id) || !Number.isSafeInteger(value.execution.used)
65
- || value.execution.used < 0 || value.execution.used > 100 || typeof value.execution.settled !== "boolean"))) {
66
- throw new Error("The Devin session state is corrupt or an unsupported version. Start a new session.");
67
- }
68
- if (value.sessionId !== sessionId) return { version: 1, sessionId, cascadeId: randomUUID(), trajectoryId: randomUUID(), nextStepIndex: 0 };
69
- return structuredClone(value);
70
- }
71
-
72
- /** One instance owns one Pi runtime. It never builds its own tool or retry loop. */
73
- export class DevinLifecycle {
74
- private binding?: SessionBinding;
75
- private state: DevinSessionState = { version: 1, sessionId: randomUUID(), cascadeId: randomUUID(), trajectoryId: randomUUID(), nextStepIndex: 0 };
76
- private execution?: Execution;
77
- private isClosed = false;
78
- private isAuxiliary = false;
79
- readonly maxModelCalls: number;
80
- constructor(maxModelCalls = 8) {
81
- if (!Number.isSafeInteger(maxModelCalls) || maxModelCalls < 1 || maxModelCalls > 100) throw new Error("The Devin request budget must be between 1 and 100.");
82
- this.maxModelCalls = maxModelCalls;
83
- }
84
- snapshot(): DevinSessionState { return structuredClone(this.state); }
85
- /** Model requests of the running or last finished run. used is 0 when this session has no run yet. */
86
- budget(): { used: number; limit: number; isActive: boolean } {
87
- const execution = this.state.execution;
88
- return { used: execution?.used ?? 0, limit: this.maxModelCalls, isActive: execution !== undefined && !execution.settled };
89
- }
90
- private checkpoint(): void {
91
- if (this.binding && this.binding.getSessionId() !== this.state.sessionId) throw cancelled();
92
- this.binding?.append(structuredClone(this.state));
93
- }
94
- private activate(data: NonNullable<DevinSessionState["execution"]>, hasCheckpoint = false): Execution {
95
- this.state.execution = data;
96
- return this.execution = { data, controller: new AbortController(), tail: Promise.resolve(), cleanups: new Set(), activeRequests: 0, hasStarted: false, nextIsRecovery: false, hasCheckpoint };
97
- }
98
- attach(binding: SessionBinding): void {
99
- this.abort(cancelled());
100
- this.binding = binding;
101
- this.state = restoreState(binding);
102
- this.execution = undefined; this.isClosed = false; this.isAuxiliary = false;
103
- if (this.state.execution && !this.state.execution.settled) {
104
- this.activate({ ...this.state.execution, kind: "recovered" }, true);
105
- }
106
- }
107
- begin(kind: ExecutionKind): void {
108
- if (this.isClosed) throw cancelled();
109
- if (this.execution && !this.execution.data.settled) {
110
- if (kind === "agent" && ["pending", "recovered"].includes(this.execution.data.kind)) this.execution.data.kind = "agent";
111
- else if (kind === "agent" && ["manual", "tree", "direct"].includes(this.execution.data.kind) && this.execution.activeRequests === 0) this.finish();
112
- else return; // Retries and tool follow-up calls do not get a new budget.
113
- if (this.execution) return;
114
- }
115
- this.activate({ id: randomUUID(), used: 0, settled: false, kind });
116
- }
117
- agentStarted(): void {
118
- if (!this.execution) this.begin("agent");
119
- this.execution!.nextIsRecovery = this.execution!.hasStarted;
120
- this.execution!.hasStarted = true;
121
- }
122
- beforeCompaction(reason: "manual" | "threshold" | "overflow"): void {
123
- this.begin(reason === "manual" ? "manual" : "pending");
124
- this.isAuxiliary = true;
125
- }
126
- afterCompaction(reason: "manual" | "threshold" | "overflow"): void {
127
- this.isAuxiliary = false;
128
- if (reason === "manual" && this.execution?.data.kind === "manual") this.finish();
129
- }
130
- beforeTree(): void { this.begin("tree"); this.isAuxiliary = true; }
131
- finish(): void {
132
- if (!this.execution) return;
133
- this.execution.data.settled = true;
134
- this.abort(cancelled());
135
- if (this.execution.hasCheckpoint) this.checkpoint();
136
- this.execution = undefined; this.isAuxiliary = false;
137
- }
138
- abort(reason: unknown): void {
139
- const active = this.execution;
140
- if (!active) return;
141
- if (!active.controller.signal.aborted) active.controller.abort(reason);
142
- for (const cleanup of active.cleanups) { try { cleanup(); } catch { /* A cleanup error must not skip releasing other resources. */ } }
143
- }
144
- shutdown(): void {
145
- if (this.isClosed) return;
146
- this.abort(cancelled());
147
- // An unfinished budget stays in the checkpoint so a reload cannot reset the limit.
148
- this.isClosed = true;
149
- }
150
- treeChanged(): void {
151
- if (!this.binding) return;
152
- this.abort(cancelled());
153
- this.state = restoreState(this.binding);
154
- this.state.trajectoryId = randomUUID(); this.state.nextStepIndex = 0;
155
- this.state.execution = undefined; this.execution = undefined; this.isAuxiliary = false;
156
- if (this.state.ownerHash) this.checkpoint();
157
- }
158
- async acquire(ownerHash: string, callerSignal?: AbortSignal): Promise<RequestLease> {
159
- if (this.isClosed) throw cancelled();
160
- callerSignal?.throwIfAborted();
161
- if (!/^[a-f0-9]{64}$/.test(ownerHash)) throw new Error("A verified Devin account scope is required.");
162
- if (this.state.ownerHash && this.state.ownerHash !== ownerHash) throw new Error("This Pi session is bound to a different Devin account or endpoint. Start a new session.");
163
- this.state.ownerHash = ownerHash;
164
- if (!this.execution) this.begin("direct");
165
- const active = this.execution!, state = this.state;
166
- const signal = AbortSignal.any([active.controller.signal, ...(callerSignal ? [callerSignal] : [])]);
167
- const previous = active.tail;
168
- let releaseQueue!: () => void;
169
- const completed = new Promise<void>((resolve) => { releaseQueue = resolve; });
170
- active.tail = previous.then(() => completed);
171
- let hasReleased = false, hasAcquired = false;
172
- const ownCleanups = new Set<() => void>();
173
- const clean = () => { for (const cleanup of ownCleanups) { try { cleanup(); } catch {} } };
174
- const assertCurrent = () => {
175
- signal.throwIfAborted();
176
- if (this.execution !== active || this.state !== state || this.isClosed || (this.binding && this.binding.getSessionId() !== state.sessionId)) throw cancelled();
177
- };
178
- const release = () => {
179
- if (hasReleased) return;
180
- hasReleased = true;
181
- signal.removeEventListener("abort", clean);
182
- for (const cleanup of ownCleanups) { active.cleanups.delete(cleanup); try { cleanup(); } catch {} }
183
- if (hasAcquired) active.activeRequests--;
184
- releaseQueue();
185
- };
186
- try {
187
- await awaitWithSignal(previous, signal);
188
- assertCurrent();
189
- if (active.data.used >= this.maxModelCalls) throw new ModelBudgetError(this.maxModelCalls);
190
- if (state.nextStepIndex >= 2_147_483_647) throw new Error("The Devin step number limit was reached. Start a new session.");
191
- active.activeRequests++; hasAcquired = true;
192
- const stepIndex = state.nextStepIndex++;
193
- active.hasCheckpoint = true;
194
- this.checkpoint();
195
- signal.addEventListener("abort", clean, { once: true });
196
- const isAuxiliary = this.isAuxiliary;
197
- const isRecovery = !isAuxiliary && active.nextIsRecovery;
198
- if (!isAuxiliary) active.nextIsRecovery = false;
199
- return {
200
- signal, executionId: active.data.id, cascadeId: state.cascadeId, trajectoryId: state.trajectoryId,
201
- sessionId: state.sessionId, stepIndex,
202
- isAuxiliary, isRecovery,
203
- consume: () => {
204
- assertCurrent();
205
- if (active.data.used >= this.maxModelCalls) { const error = new ModelBudgetError(this.maxModelCalls); this.abort(error); throw error; }
206
- active.data.used++; this.checkpoint();
207
- },
208
- addCleanup: (cleanup) => {
209
- let hasCleaned = false;
210
- const once = () => { if (!hasCleaned) { hasCleaned = true; cleanup(); } };
211
- ownCleanups.add(once); active.cleanups.add(once); if (signal.aborted) once();
212
- },
213
- release,
214
- };
215
- } catch (error) { release(); throw error; }
216
- }
217
- }
@@ -1,131 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { getCurrentSystemPrompt, getCurrentTools, type AssistantMessage, type TranscriptContext } from "@earendil-works/pi-ai";
3
- import { imageFromBase64, MAX_IMAGES_BYTES } from "../core/inference/images.ts";
4
- import { object, requiredString, type ChatMessage, type ChatTool, type ModelInfo } from "../core/types.ts";
5
- import { DEVIN_API } from "./models.ts";
6
-
7
- const REPLAY_PREFIX = "devin:v1:";
8
- const replayStrings = ["signature", "signatureType", "geminiThoughtSignature", "outputId", "thinkingId", "phase", "modelUid"] as const;
9
- type ReplayMetadata = Pick<ChatMessage, typeof replayStrings[number] | "thinkingRedacted">;
10
-
11
- export function encodeReplayMetadata(message: ChatMessage): string {
12
- const metadata: ReplayMetadata = {};
13
- for (const key of replayStrings) if (message[key] !== undefined) metadata[key] = message[key];
14
- if (message.thinkingRedacted !== undefined) metadata.thinkingRedacted = message.thinkingRedacted;
15
- return REPLAY_PREFIX + JSON.stringify(metadata);
16
- }
17
- function replayMetadata(value: string | undefined): ReplayMetadata | undefined {
18
- if (!value?.startsWith(REPLAY_PREFIX)) return undefined;
19
- let parsed: Record<string, unknown>;
20
- try { parsed = object(JSON.parse(value.slice(REPLAY_PREFIX.length)), "Devin replay metadata"); }
21
- catch { throw new Error("Invalid Devin response replay metadata."); }
22
- const result: ReplayMetadata = {};
23
- for (const key of replayStrings) {
24
- if (parsed[key] !== undefined && typeof parsed[key] !== "string") throw new Error("Unexpected Devin response replay metadata format.");
25
- if (typeof parsed[key] === "string") result[key] = parsed[key];
26
- }
27
- if (parsed.thinkingRedacted !== undefined) {
28
- if (typeof parsed.thinkingRedacted !== "boolean") throw new Error("Invalid Devin redacted thinking metadata.");
29
- result.thinkingRedacted = parsed.thinkingRedacted;
30
- }
31
- return result;
32
- }
33
-
34
- export function attachReplayMetadata(output: AssistantMessage, message: ChatMessage): void {
35
- const signature = encodeReplayMetadata(message);
36
- for (const block of output.content) {
37
- if (block.type === "thinking") block.thinkingSignature = signature;
38
- else if (block.type === "text") block.textSignature = signature;
39
- else block.thoughtSignature = signature;
40
- }
41
- }
42
-
43
- const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]);
44
- const SCHEMA_DATA = new Set(["enum", "const", "default", "examples", "example"]);
45
-
46
- /** Devin's own tools give every schema a single `type`; its Gemini route rejects a list such as `["integer", "null"]`. */
47
- function toDevinToolSchema(schema: unknown): unknown {
48
- if (Array.isArray(schema)) return schema.map(toDevinToolSchema);
49
- if (!schema || typeof schema !== "object") return schema;
50
- const result: Record<string, unknown> = {};
51
- for (const [key, value] of Object.entries(schema)) {
52
- if (SCHEMA_DATA.has(key)) result[key] = value;
53
- else if (SCHEMA_MAPS.has(key) && value && typeof value === "object" && !Array.isArray(value)) {
54
- result[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toDevinToolSchema(child)]));
55
- } else if (key === "type" && Array.isArray(value)) {
56
- // Omitting the field still covers null; several remaining types become one branch each.
57
- const types = value.filter((type) => type !== "null");
58
- if (types.length === 1) result.type = types[0];
59
- else if (types.length > 1 && !("anyOf" in schema)) result.anyOf = types.map((type) => ({ type }));
60
- else result.type = value;
61
- } else result[key] = toDevinToolSchema(value);
62
- }
63
- return result;
64
- }
65
-
66
- export function toDevinTranscript(context: TranscriptContext, model: ModelInfo): { messages: ChatMessage[]; tools: ChatTool[]; systemPrompt: string } {
67
- const declarations = getCurrentTools(context.messages);
68
- if (declarations.length && !model.supportsToolCalls) throw new Error("This model does not support the current Pi tool declarations.");
69
- const tools = declarations.map((tool) => {
70
- // Grammar sampling is optional; like Pi's providers without grammar tools, send the JSON schema instead.
71
- if (tool.constrainedSampling && tool.constrainedSampling.type === "json_schema" && tool.constrainedSampling.strict === "require") {
72
- throw new Error(`Devin does not support this tool's forced sampling format: ${tool.name}`);
73
- }
74
- return { name: tool.name, description: tool.description, jsonSchemaString: JSON.stringify(toDevinToolSchema(tool.parameters)) };
75
- });
76
- const failedCalls = new Set(context.messages.flatMap((message) => message.role === "assistant" && ["error", "aborted"].includes(message.stopReason)
77
- ? message.content.filter((block) => block.type === "toolCall").map((block) => block.id) : []));
78
- const messages: ChatMessage[] = [];
79
- for (const message of context.messages) {
80
- if (message.role === "system") continue;
81
- if (message.role === "assistant") {
82
- if (message.stopReason === "error" || message.stopReason === "aborted") continue;
83
- if (message.stopReason === "pending") throw new Error("An unfinished assistant response cannot be sent as Devin history.");
84
- const own = message.provider === "devin" && message.api === DEVIN_API;
85
- const result: ChatMessage = { messageId: message.responseId ?? randomUUID(), source: "CHAT_MESSAGE_SOURCE_SYSTEM", prompt: "" };
86
- let replay: ReplayMetadata | undefined;
87
- for (const block of message.content) {
88
- if (block.type === "text") result.prompt += block.text;
89
- else if (block.type === "thinking") {
90
- if (!block.redacted) result.thinking = (result.thinking ?? "") + block.thinking;
91
- else result.thinkingRedacted = true;
92
- } else {
93
- result.toolCalls ??= [];
94
- result.toolCalls.push({ id: requiredString(block.id, "tool call id"), name: block.namespace ? `${block.namespace}.${block.name}` : block.name,
95
- argumentsJson: JSON.stringify(object(block.arguments, "tool arguments")) });
96
- }
97
- if (own) {
98
- const metadata = replayMetadata(block.type === "text" ? block.textSignature : block.type === "thinking" ? block.thinkingSignature : block.thoughtSignature);
99
- if (metadata) {
100
- if (replay && JSON.stringify(replay) !== JSON.stringify(metadata)) throw new Error("Devin replay metadata differs within one response.");
101
- replay = metadata;
102
- }
103
- }
104
- }
105
- Object.assign(result, replay);
106
- if (own && message.providerThinkingLevel) result.modelUid = message.providerThinkingLevel;
107
- if (result.thinkingRedacted) result.thinking = "";
108
- messages.push(result);
109
- continue;
110
- }
111
- if (message.role === "toolResult" && failedCalls.has(message.toolCallId)) continue;
112
- const result: ChatMessage = {
113
- messageId: randomUUID(), source: message.role === "toolResult" ? "CHAT_MESSAGE_SOURCE_TOOL" : "CHAT_MESSAGE_SOURCE_USER", prompt: "",
114
- ...(message.role === "toolResult" ? { toolCallId: message.toolCallId, toolResultIsError: message.isError } : {}),
115
- };
116
- let imageBytes = 0;
117
- const content = typeof message.content === "string" ? [{ type: "text" as const, text: message.content }] : message.content;
118
- for (const block of content) {
119
- if (block.type === "text") result.prompt += block.text;
120
- else {
121
- if (!model.supportsImages) throw new Error("This model does not support images in history or tool results.");
122
- const converted = imageFromBase64(block.data, block.mimeType);
123
- imageBytes += converted.sizeBytes;
124
- if (imageBytes > MAX_IMAGES_BYTES) throw new Error(`Images in one message must total at most ${MAX_IMAGES_BYTES} bytes.`);
125
- (result.images ??= []).push(converted.image);
126
- }
127
- }
128
- messages.push(result);
129
- }
130
- return { messages, tools, systemPrompt: getCurrentSystemPrompt(context.messages) };
131
- }