pi-ultracode 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -14
- package/extensions/ultracode.ts +23 -8
- package/package.json +22 -1
- package/src/commands.ts +27 -84
- package/src/index.ts +20 -0
- package/src/mode.ts +32 -6
- package/src/prompts.ts +1 -1
- package/src/workflow/agent-runner.ts +515 -39
- package/src/workflow/display-text.ts +17 -6
- package/src/workflow/display.ts +104 -20
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/registry.ts +106 -10
- package/src/workflow/run-details.ts +1276 -0
- package/src/workflow/runtime.ts +175 -26
- package/src/workflow/tool.ts +180 -14
- package/src/workflow/workflow-overlay.ts +719 -0
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* allowlist), and an alternate cwd for git-worktree isolation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
12
14
|
import {
|
|
13
15
|
createAgentSession,
|
|
14
16
|
createCodingTools,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
SessionManager,
|
|
17
19
|
SettingsManager,
|
|
18
20
|
VERSION as PI_VERSION,
|
|
21
|
+
type CreateAgentSessionOptions,
|
|
19
22
|
type ToolDefinition,
|
|
20
23
|
} from "@earendil-works/pi-coding-agent";
|
|
21
24
|
import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
|
|
@@ -27,6 +30,7 @@ import {
|
|
|
27
30
|
redactCommand,
|
|
28
31
|
safeCommandPreview,
|
|
29
32
|
safeDisplayText,
|
|
33
|
+
safeTranscriptText,
|
|
30
34
|
truncateDisplay,
|
|
31
35
|
} from "./display-text.ts";
|
|
32
36
|
import { jsonSchemaToTypeBox } from "./json-schema.ts";
|
|
@@ -52,22 +56,98 @@ export interface ModelLike {
|
|
|
52
56
|
|
|
53
57
|
export interface ModelRegistryLike {
|
|
54
58
|
getAvailable(): ModelLike[];
|
|
59
|
+
/** Legacy public projection retained for consumers of this structural type. */
|
|
55
60
|
getAll?(): ModelLike[];
|
|
61
|
+
getRegisteredProviderIds?(): readonly string[];
|
|
62
|
+
getRegisteredProviderConfig?(provider: string): unknown;
|
|
63
|
+
getProviderAuthStatus?(provider: string): {
|
|
64
|
+
configured: boolean;
|
|
65
|
+
source?: string;
|
|
66
|
+
};
|
|
67
|
+
getApiKeyForProvider?(provider: string): Promise<string | undefined>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Structural ModelRuntime seam that remains loadable on pre-0.80.8 Pi. */
|
|
71
|
+
export interface ModelRuntimeLike {
|
|
72
|
+
getModel?(provider: string, modelId: string): ModelLike | undefined;
|
|
73
|
+
registerProvider?(provider: string, config: any): void;
|
|
74
|
+
refresh?(options?: { allowNetwork?: boolean }): Promise<unknown>;
|
|
75
|
+
setRuntimeApiKey?(provider: string, apiKey: string): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ModelRuntimeCreateOptions {
|
|
79
|
+
authPath: string;
|
|
80
|
+
modelsPath: string;
|
|
81
|
+
/** Child sessions reuse the parent's catalog snapshot and must not refresh remotely. */
|
|
82
|
+
allowModelNetwork: false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ModelRuntimeFactory = (
|
|
86
|
+
options: ModelRuntimeCreateOptions,
|
|
87
|
+
) => Promise<ModelRuntimeLike | undefined>;
|
|
88
|
+
|
|
89
|
+
export interface AgentTurnUsage {
|
|
90
|
+
inputTokens: number;
|
|
91
|
+
outputTokens: number;
|
|
92
|
+
cacheReadTokens: number;
|
|
93
|
+
cacheWriteTokens: number;
|
|
94
|
+
totalTokens: number;
|
|
95
|
+
cost: number;
|
|
56
96
|
}
|
|
57
97
|
|
|
58
98
|
export interface AgentUsage {
|
|
99
|
+
/** Optional on injected legacy runners; production runners always provide it. */
|
|
100
|
+
inputTokens?: number;
|
|
59
101
|
outputTokens: number;
|
|
102
|
+
/** Compact token use: input + output, excluding cache traffic. */
|
|
60
103
|
totalTokens: number;
|
|
104
|
+
cacheReadTokens?: number;
|
|
105
|
+
cacheWriteTokens?: number;
|
|
61
106
|
cost: number;
|
|
107
|
+
/** Completed assistant messages. */
|
|
108
|
+
turns?: number;
|
|
109
|
+
/** Tool executions that reached tool_execution_start. */
|
|
110
|
+
toolUses?: number;
|
|
111
|
+
retries?: number;
|
|
112
|
+
compactions?: number;
|
|
62
113
|
}
|
|
63
114
|
|
|
64
115
|
export interface AgentRunResult {
|
|
65
116
|
value: unknown;
|
|
66
117
|
usage: AgentUsage;
|
|
118
|
+
/** Model id and effort actually applied by the child session. */
|
|
119
|
+
modelId?: string;
|
|
120
|
+
effort?: ThinkingLevel;
|
|
67
121
|
/** cwd the agent actually ran in (differs from the shared cwd under worktree isolation). */
|
|
68
122
|
cwd: string;
|
|
69
123
|
}
|
|
70
124
|
|
|
125
|
+
/** Detailed child-session events consumed by the private transcript store. */
|
|
126
|
+
export type AgentTelemetryEvent =
|
|
127
|
+
| { kind: "model_requested"; modelId?: string; effort?: ThinkingLevel }
|
|
128
|
+
| { kind: "model_resolved"; modelId?: string; effort: ThinkingLevel }
|
|
129
|
+
| { kind: "turn_start"; turnIndex?: number }
|
|
130
|
+
| { kind: "text_delta"; delta: string }
|
|
131
|
+
| { kind: "message_end"; text: string; usage?: AgentTurnUsage; error?: string }
|
|
132
|
+
| { kind: "thinking_start" | "thinking_end" }
|
|
133
|
+
| { kind: "tool_start"; toolCallId: string; toolName: string; toolArgs?: string }
|
|
134
|
+
| {
|
|
135
|
+
kind: "tool_end";
|
|
136
|
+
toolCallId: string;
|
|
137
|
+
toolName: string;
|
|
138
|
+
isError: boolean;
|
|
139
|
+
resultPreview?: string;
|
|
140
|
+
}
|
|
141
|
+
| { kind: "retry"; state: "start" | "end"; detail: string }
|
|
142
|
+
| { kind: "compaction"; state: "start" | "end"; detail: string }
|
|
143
|
+
| {
|
|
144
|
+
kind: "run_error";
|
|
145
|
+
error: string;
|
|
146
|
+
usage: AgentUsage;
|
|
147
|
+
modelId?: string;
|
|
148
|
+
effort?: ThinkingLevel;
|
|
149
|
+
};
|
|
150
|
+
|
|
71
151
|
export interface AgentSessionLike {
|
|
72
152
|
thinkingLevel: ThinkingLevel;
|
|
73
153
|
model?: ModelLike;
|
|
@@ -83,19 +163,35 @@ export interface AgentSessionLike {
|
|
|
83
163
|
getSessionStats?(): unknown;
|
|
84
164
|
}
|
|
85
165
|
|
|
166
|
+
export type AgentSessionCreateOptions = Omit<
|
|
167
|
+
CreateAgentSessionOptions,
|
|
168
|
+
"model" | "modelRegistry" | "modelRuntime"
|
|
169
|
+
> & {
|
|
170
|
+
model?: ModelLike;
|
|
171
|
+
/** Legacy Pi option, selected only when ModelRuntime is unavailable. */
|
|
172
|
+
modelRegistry?: ModelRegistryLike;
|
|
173
|
+
/** Pi 0.80.8+ canonical model/auth runtime. */
|
|
174
|
+
modelRuntime?: ModelRuntimeLike;
|
|
175
|
+
};
|
|
176
|
+
|
|
86
177
|
export type AgentSessionFactory = (
|
|
87
|
-
options:
|
|
178
|
+
options: AgentSessionCreateOptions,
|
|
88
179
|
) => Promise<{ session: AgentSessionLike }>;
|
|
89
180
|
|
|
90
181
|
export interface WorkflowAgentRunnerOptions {
|
|
91
182
|
cwd: string;
|
|
183
|
+
/** Synchronous extension facade used only for model selection and state replay. */
|
|
92
184
|
modelRegistry?: ModelRegistryLike;
|
|
185
|
+
/** Canonical runtime to share across child sessions when supplied by an SDK host. */
|
|
186
|
+
modelRuntime?: ModelRuntimeLike;
|
|
93
187
|
/** Default model used when an agent() call does not override it. */
|
|
94
188
|
model?: ModelLike;
|
|
95
189
|
/** Default thinking level for subagents. */
|
|
96
190
|
thinkingLevel?: ThinkingLevel;
|
|
97
191
|
/** Test seam for session construction and initialization races. */
|
|
98
192
|
createSession?: AgentSessionFactory;
|
|
193
|
+
/** Test/compatibility seam for async ModelRuntime initialization. */
|
|
194
|
+
createModelRuntime?: ModelRuntimeFactory;
|
|
99
195
|
/** Override runtime feature detection for pre-max Pi compatibility tests. */
|
|
100
196
|
supportsMaxThinking?: boolean;
|
|
101
197
|
}
|
|
@@ -130,24 +226,33 @@ export interface AgentRunCall {
|
|
|
130
226
|
agentTypeDef?: AgentTypeDef;
|
|
131
227
|
/** Override cwd (worktree). */
|
|
132
228
|
cwd?: string;
|
|
133
|
-
/**
|
|
229
|
+
/** Safe compact activity stream used by the inline workflow status. */
|
|
134
230
|
onActivity?: (event: AgentActivityInput) => void;
|
|
231
|
+
/** Private detailed stream used by the task transcript store. */
|
|
232
|
+
onTelemetry?: (event: AgentTelemetryEvent) => void;
|
|
135
233
|
}
|
|
136
234
|
|
|
137
235
|
export class WorkflowAgentRunner {
|
|
138
236
|
private readonly baseCwd: string;
|
|
139
237
|
private readonly modelRegistry?: ModelRegistryLike;
|
|
238
|
+
private readonly providedModelRuntime?: ModelRuntimeLike;
|
|
140
239
|
private readonly defaultModel?: ModelLike;
|
|
141
240
|
private readonly defaultThinking?: ThinkingLevel;
|
|
142
241
|
private readonly createSession: AgentSessionFactory;
|
|
242
|
+
private readonly createModelRuntime?: ModelRuntimeFactory;
|
|
243
|
+
private modelRuntimePromise?: Promise<ModelRuntimeLike | undefined>;
|
|
143
244
|
private readonly runtimeSupportsMaxThinking: boolean;
|
|
144
245
|
|
|
145
246
|
constructor(options: WorkflowAgentRunnerOptions) {
|
|
146
247
|
this.baseCwd = options.cwd;
|
|
147
248
|
this.modelRegistry = options.modelRegistry;
|
|
249
|
+
this.providedModelRuntime = options.modelRuntime;
|
|
148
250
|
this.defaultModel = options.model;
|
|
149
251
|
this.defaultThinking = options.thinkingLevel;
|
|
252
|
+
const usesPiSessionFactory = options.createSession === undefined;
|
|
150
253
|
this.createSession = options.createSession ?? (createAgentSession as unknown as AgentSessionFactory);
|
|
254
|
+
this.createModelRuntime = options.createModelRuntime
|
|
255
|
+
?? (options.modelRuntime !== undefined || !usesPiSessionFactory ? undefined : createPiModelRuntime);
|
|
151
256
|
this.runtimeSupportsMaxThinking = options.supportsMaxThinking ?? piVersionSupportsMaxThinking(PI_VERSION);
|
|
152
257
|
}
|
|
153
258
|
|
|
@@ -171,9 +276,30 @@ export class WorkflowAgentRunner {
|
|
|
171
276
|
if (toolAllowlist) toolAllowlist.push("structured_output");
|
|
172
277
|
}
|
|
173
278
|
|
|
174
|
-
const
|
|
175
|
-
|
|
279
|
+
const selection = this.resolveModel(call.modelPattern, call.agentTypeDef);
|
|
280
|
+
const thinkingLevel = selection.thinkingLevel;
|
|
176
281
|
const agentDir = getAgentDir();
|
|
282
|
+
|
|
283
|
+
let modelRuntime: ModelRuntimeLike | undefined;
|
|
284
|
+
let model: ModelLike | undefined;
|
|
285
|
+
try {
|
|
286
|
+
modelRuntime = await waitForSharedInitialization(
|
|
287
|
+
this.getModelRuntime(agentDir),
|
|
288
|
+
call.signal,
|
|
289
|
+
);
|
|
290
|
+
if (call.signal?.aborted) throw abortedError();
|
|
291
|
+
model = selection.model && modelRuntime?.getModel
|
|
292
|
+
? modelRuntime.getModel(selection.model.provider, selection.model.id) ?? selection.model
|
|
293
|
+
: selection.model;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
safeEmitTelemetry(call.onTelemetry, {
|
|
296
|
+
kind: "run_error",
|
|
297
|
+
error: safeDisplayText(errorText(error), 512),
|
|
298
|
+
usage: emptyAgentUsage(),
|
|
299
|
+
});
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
|
|
177
303
|
const createSession = (level: ThinkingLevel | undefined) => this.createSession({
|
|
178
304
|
cwd,
|
|
179
305
|
agentDir,
|
|
@@ -183,34 +309,62 @@ export class WorkflowAgentRunner {
|
|
|
183
309
|
...(model ? { model: model as any } : {}),
|
|
184
310
|
...(level ? { thinkingLevel: level as any } : {}),
|
|
185
311
|
...(toolAllowlist ? { tools: toolAllowlist } : {}),
|
|
186
|
-
...(
|
|
312
|
+
...(modelRuntime
|
|
313
|
+
? { modelRuntime }
|
|
314
|
+
: this.modelRegistry
|
|
315
|
+
? { modelRegistry: this.modelRegistry as any }
|
|
316
|
+
: {}),
|
|
187
317
|
});
|
|
188
318
|
|
|
189
319
|
const sessionThinking = resolveSessionThinkingLevel(thinkingLevel, model);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
sessionThinking === ULTRACODE_THINKING_LEVEL &&
|
|
200
|
-
created.session.thinkingLevel !== ULTRACODE_THINKING_LEVEL &&
|
|
201
|
-
created.session.supportsThinking() &&
|
|
202
|
-
(!this.runtimeSupportsMaxThinking || selectedModelAdvertisesMax)
|
|
203
|
-
) {
|
|
204
|
-
// A legacy runtime may clamp an unknown `max` to medium/high instead of
|
|
205
|
-
// off. Recreate with xhigh; never mutate the user's global default effort.
|
|
206
|
-
disposeQuietly(created.session);
|
|
207
|
-
created = await createSession(LEGACY_ULTRACODE_THINKING_LEVEL);
|
|
320
|
+
safeEmitTelemetry(call.onTelemetry, {
|
|
321
|
+
kind: "model_requested",
|
|
322
|
+
modelId: model?.id,
|
|
323
|
+
effort: thinkingLevel,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
let created: { session: AgentSessionLike };
|
|
327
|
+
try {
|
|
328
|
+
created = await createSession(sessionThinking);
|
|
208
329
|
if (call.signal?.aborted) {
|
|
209
330
|
disposeQuietly(created.session);
|
|
210
331
|
throw abortedError();
|
|
211
332
|
}
|
|
333
|
+
const selectedModel = created.session.model;
|
|
334
|
+
const selectedModelAdvertisesMax = (model ?? selectedModel)?.thinkingLevelMap?.max != null;
|
|
335
|
+
if (
|
|
336
|
+
thinkingLevel === ULTRACODE_THINKING_LEVEL &&
|
|
337
|
+
sessionThinking === ULTRACODE_THINKING_LEVEL &&
|
|
338
|
+
created.session.thinkingLevel !== ULTRACODE_THINKING_LEVEL &&
|
|
339
|
+
created.session.supportsThinking() &&
|
|
340
|
+
(!this.runtimeSupportsMaxThinking || selectedModelAdvertisesMax)
|
|
341
|
+
) {
|
|
342
|
+
// A legacy runtime may clamp an unknown `max` to medium/high instead of
|
|
343
|
+
// off. Recreate with xhigh; never mutate the user's global default effort.
|
|
344
|
+
disposeQuietly(created.session);
|
|
345
|
+
created = await createSession(LEGACY_ULTRACODE_THINKING_LEVEL);
|
|
346
|
+
if (call.signal?.aborted) {
|
|
347
|
+
disposeQuietly(created.session);
|
|
348
|
+
throw abortedError();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
safeEmitTelemetry(call.onTelemetry, {
|
|
353
|
+
kind: "run_error",
|
|
354
|
+
error: safeDisplayText(errorText(error), 512),
|
|
355
|
+
usage: emptyAgentUsage(),
|
|
356
|
+
});
|
|
357
|
+
throw error;
|
|
212
358
|
}
|
|
213
359
|
const { session } = created;
|
|
360
|
+
const actualModelId = session.model?.id ?? model?.id;
|
|
361
|
+
const actualEffort = session.thinkingLevel;
|
|
362
|
+
const telemetryCounters = { retries: 0, compactions: 0, turns: 0, toolUses: 0, observing: false };
|
|
363
|
+
safeEmitTelemetry(call.onTelemetry, {
|
|
364
|
+
kind: "model_resolved",
|
|
365
|
+
modelId: actualModelId,
|
|
366
|
+
effort: actualEffort,
|
|
367
|
+
});
|
|
214
368
|
|
|
215
369
|
let removeAbort: (() => void) | undefined;
|
|
216
370
|
let unsubscribe: (() => void) | undefined;
|
|
@@ -234,11 +388,21 @@ export class WorkflowAgentRunner {
|
|
|
234
388
|
}
|
|
235
389
|
}
|
|
236
390
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
391
|
+
// One subscription feeds both compact status and the private transcript
|
|
392
|
+
// stream. Telemetry callbacks are isolated so observability can never
|
|
393
|
+
// change the child run's outcome.
|
|
394
|
+
if (call.onActivity || call.onTelemetry) {
|
|
395
|
+
telemetryCounters.observing = true;
|
|
396
|
+
unsubscribe = session.subscribe((event: unknown) => {
|
|
397
|
+
const sessionEvent = event as { type?: unknown; message?: { role?: unknown } };
|
|
398
|
+
const eventType = sessionEvent?.type;
|
|
399
|
+
if (eventType === "message_end" && sessionEvent.message?.role === "assistant") telemetryCounters.turns++;
|
|
400
|
+
if (eventType === "tool_execution_start") telemetryCounters.toolUses++;
|
|
401
|
+
if (eventType === "auto_retry_start") telemetryCounters.retries++;
|
|
402
|
+
if (eventType === "compaction_start") telemetryCounters.compactions++;
|
|
403
|
+
if (call.onActivity) forwardActivity(event, call.onActivity);
|
|
404
|
+
if (call.onTelemetry) forwardTelemetry(event, call.onTelemetry);
|
|
405
|
+
});
|
|
242
406
|
}
|
|
243
407
|
|
|
244
408
|
await session.prompt(this.buildPrompt(call, Boolean(call.schema)), {
|
|
@@ -262,9 +426,22 @@ export class WorkflowAgentRunner {
|
|
|
262
426
|
value = lastAssistantText(session.messages as unknown[]);
|
|
263
427
|
}
|
|
264
428
|
|
|
265
|
-
return {
|
|
429
|
+
return {
|
|
430
|
+
value,
|
|
431
|
+
usage: readUsage(session, telemetryCounters),
|
|
432
|
+
modelId: actualModelId,
|
|
433
|
+
effort: actualEffort,
|
|
434
|
+
cwd,
|
|
435
|
+
};
|
|
266
436
|
} catch (error) {
|
|
267
437
|
hasPrimaryError = true;
|
|
438
|
+
safeEmitTelemetry(call.onTelemetry, {
|
|
439
|
+
kind: "run_error",
|
|
440
|
+
error: safeDisplayText(errorText(error), 512),
|
|
441
|
+
usage: readUsage(session, telemetryCounters),
|
|
442
|
+
modelId: actualModelId,
|
|
443
|
+
effort: actualEffort,
|
|
444
|
+
});
|
|
268
445
|
throw error;
|
|
269
446
|
} finally {
|
|
270
447
|
let hasCleanupError = false;
|
|
@@ -315,6 +492,61 @@ export class WorkflowAgentRunner {
|
|
|
315
492
|
return parts.filter(Boolean).join("\n\n");
|
|
316
493
|
}
|
|
317
494
|
|
|
495
|
+
private getModelRuntime(agentDir: string): Promise<ModelRuntimeLike | undefined> {
|
|
496
|
+
if (this.providedModelRuntime) return Promise.resolve(this.providedModelRuntime);
|
|
497
|
+
if (!this.createModelRuntime) return Promise.resolve(undefined);
|
|
498
|
+
if (!this.modelRuntimePromise) {
|
|
499
|
+
// Cache the in-flight promise so parallel agent() calls never initialize
|
|
500
|
+
// separate runtimes or race provider/auth replay. Clear only this failed
|
|
501
|
+
// attempt so a later serial agent can retry transient initialization errors.
|
|
502
|
+
const pending = this.initializeModelRuntime(agentDir);
|
|
503
|
+
this.modelRuntimePromise = pending;
|
|
504
|
+
void pending.catch(() => {
|
|
505
|
+
if (this.modelRuntimePromise === pending) this.modelRuntimePromise = undefined;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return this.modelRuntimePromise;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async initializeModelRuntime(agentDir: string): Promise<ModelRuntimeLike | undefined> {
|
|
512
|
+
const runtime = await this.createModelRuntime?.({
|
|
513
|
+
authPath: path.join(agentDir, "auth.json"),
|
|
514
|
+
modelsPath: path.join(agentDir, "models.json"),
|
|
515
|
+
allowModelNetwork: false,
|
|
516
|
+
});
|
|
517
|
+
if (!runtime) return undefined;
|
|
518
|
+
|
|
519
|
+
const registeredProviderIds = this.modelRegistry?.getRegisteredProviderIds?.() ?? [];
|
|
520
|
+
let providersChanged = false;
|
|
521
|
+
for (const provider of registeredProviderIds) {
|
|
522
|
+
const config = this.modelRegistry?.getRegisteredProviderConfig?.(provider);
|
|
523
|
+
if (config === undefined || !runtime.registerProvider) continue;
|
|
524
|
+
runtime.registerProvider(provider, config);
|
|
525
|
+
providersChanged = true;
|
|
526
|
+
}
|
|
527
|
+
if (providersChanged && runtime.refresh) {
|
|
528
|
+
await runtime.refresh({ allowNetwork: false });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Only replay CLI/SDK runtime overrides. Stored credentials, OAuth, env,
|
|
532
|
+
// and models.json auth must stay owned by the new runtime so they can refresh.
|
|
533
|
+
if (
|
|
534
|
+
runtime.setRuntimeApiKey
|
|
535
|
+
&& this.modelRegistry?.getProviderAuthStatus
|
|
536
|
+
&& this.modelRegistry.getApiKeyForProvider
|
|
537
|
+
) {
|
|
538
|
+
const providers = new Set(this.modelRegistry.getAvailable().map((model) => model.provider));
|
|
539
|
+
for (const provider of registeredProviderIds) providers.add(provider);
|
|
540
|
+
for (const provider of providers) {
|
|
541
|
+
if (this.modelRegistry.getProviderAuthStatus(provider).source !== "runtime") continue;
|
|
542
|
+
const apiKey = await this.modelRegistry.getApiKeyForProvider(provider);
|
|
543
|
+
if (apiKey) await runtime.setRuntimeApiKey(provider, apiKey);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return runtime;
|
|
548
|
+
}
|
|
549
|
+
|
|
318
550
|
private resolveModel(
|
|
319
551
|
pattern: string | undefined,
|
|
320
552
|
role: AgentTypeDef | undefined,
|
|
@@ -330,6 +562,20 @@ export class WorkflowAgentRunner {
|
|
|
330
562
|
}
|
|
331
563
|
}
|
|
332
564
|
|
|
565
|
+
interface ModelRuntimeConstructorLike {
|
|
566
|
+
create(options: ModelRuntimeCreateOptions): Promise<ModelRuntimeLike>;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Capability detection avoids importing a named export that Pi 0.80.7 lacks. */
|
|
570
|
+
async function createPiModelRuntime(
|
|
571
|
+
options: ModelRuntimeCreateOptions,
|
|
572
|
+
): Promise<ModelRuntimeLike | undefined> {
|
|
573
|
+
const runtimeClass = (PiCodingAgent as unknown as {
|
|
574
|
+
ModelRuntime?: ModelRuntimeConstructorLike;
|
|
575
|
+
}).ModelRuntime;
|
|
576
|
+
return runtimeClass?.create ? runtimeClass.create(options) : undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
333
579
|
export function splitThinkingSuffix(pattern: string): { base: string; thinking?: ThinkingLevel } {
|
|
334
580
|
const idx = pattern.lastIndexOf(":");
|
|
335
581
|
if (idx === -1) return { base: pattern };
|
|
@@ -417,6 +663,30 @@ export function resolveModelSelection(args: {
|
|
|
417
663
|
return { model, thinkingLevel: thinking ?? roleThinking ?? defaultThinking };
|
|
418
664
|
}
|
|
419
665
|
|
|
666
|
+
function waitForSharedInitialization<T>(
|
|
667
|
+
pending: Promise<T>,
|
|
668
|
+
signal: AbortSignal | undefined,
|
|
669
|
+
): Promise<T> {
|
|
670
|
+
if (!signal) return pending;
|
|
671
|
+
if (signal.aborted) return Promise.reject(abortedError());
|
|
672
|
+
|
|
673
|
+
return new Promise<T>((resolve, reject) => {
|
|
674
|
+
let settled = false;
|
|
675
|
+
const finish = (callback: () => void) => {
|
|
676
|
+
if (settled) return;
|
|
677
|
+
settled = true;
|
|
678
|
+
signal.removeEventListener("abort", onAbort);
|
|
679
|
+
callback();
|
|
680
|
+
};
|
|
681
|
+
const onAbort = () => finish(() => reject(abortedError()));
|
|
682
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
683
|
+
pending.then(
|
|
684
|
+
(value) => finish(() => resolve(value)),
|
|
685
|
+
(error) => finish(() => reject(error)),
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
420
690
|
function abortedError(): Error {
|
|
421
691
|
return new Error("Subagent was aborted");
|
|
422
692
|
}
|
|
@@ -429,31 +699,237 @@ function disposeQuietly(session: { dispose(): void }): void {
|
|
|
429
699
|
}
|
|
430
700
|
}
|
|
431
701
|
|
|
432
|
-
function
|
|
702
|
+
function emptyAgentUsage(): AgentUsage {
|
|
703
|
+
return {
|
|
704
|
+
inputTokens: 0,
|
|
705
|
+
outputTokens: 0,
|
|
706
|
+
cacheReadTokens: 0,
|
|
707
|
+
cacheWriteTokens: 0,
|
|
708
|
+
totalTokens: 0,
|
|
709
|
+
cost: 0,
|
|
710
|
+
turns: 0,
|
|
711
|
+
toolUses: 0,
|
|
712
|
+
retries: 0,
|
|
713
|
+
compactions: 0,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function errorText(error: unknown): string {
|
|
718
|
+
return error instanceof Error ? error.message : String(error);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function safeEmitTelemetry(
|
|
722
|
+
listener: ((event: AgentTelemetryEvent) => void) | undefined,
|
|
723
|
+
event: AgentTelemetryEvent,
|
|
724
|
+
): void {
|
|
725
|
+
if (!listener) return;
|
|
726
|
+
try {
|
|
727
|
+
listener(event);
|
|
728
|
+
} catch {
|
|
729
|
+
// Detailed observability is best-effort and must never affect execution.
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function normalizeTurnUsage(value: unknown): AgentTurnUsage | undefined {
|
|
734
|
+
if (!value || typeof value !== "object") return undefined;
|
|
735
|
+
const usage = value as any;
|
|
736
|
+
const inputTokens = finiteNumber(usage.input) ?? finiteNumber(usage.inputTokens) ?? 0;
|
|
737
|
+
const outputTokens = finiteNumber(usage.output) ?? finiteNumber(usage.outputTokens) ?? 0;
|
|
738
|
+
return {
|
|
739
|
+
inputTokens,
|
|
740
|
+
outputTokens,
|
|
741
|
+
cacheReadTokens: finiteNumber(usage.cacheRead) ?? 0,
|
|
742
|
+
cacheWriteTokens: finiteNumber(usage.cacheWrite) ?? 0,
|
|
743
|
+
// Compact task stats intentionally exclude cache traffic from token use.
|
|
744
|
+
totalTokens: inputTokens + outputTokens,
|
|
745
|
+
cost: finiteNumber(usage.cost?.total) ?? finiteNumber(usage.cost) ?? 0,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function assistantText(message: any): string {
|
|
750
|
+
if (!Array.isArray(message?.content)) return "";
|
|
751
|
+
return message.content
|
|
752
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
753
|
+
.map((part: any) => part.text)
|
|
754
|
+
.join("");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function resultText(result: any): string {
|
|
758
|
+
if (!Array.isArray(result?.content)) return "";
|
|
759
|
+
return result.content
|
|
760
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
761
|
+
.map((part: any) => part.text)
|
|
762
|
+
.join("\n");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function tailByUtf8Bytes(value: string, maxBytes: number): string {
|
|
766
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
767
|
+
let low = 0;
|
|
768
|
+
let high = value.length;
|
|
769
|
+
while (low < high) {
|
|
770
|
+
const mid = Math.floor((low + high) / 2);
|
|
771
|
+
if (Buffer.byteLength(value.slice(mid), "utf8") <= maxBytes) high = mid;
|
|
772
|
+
else low = mid + 1;
|
|
773
|
+
}
|
|
774
|
+
return value.slice(low);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function toolResultPreview(result: unknown): string | undefined {
|
|
778
|
+
const raw = resultText(result);
|
|
779
|
+
if (!raw.trim()) return undefined;
|
|
780
|
+
const safe = safeTranscriptText(raw, 64 * 1024);
|
|
781
|
+
const lines = safe.split(/\r?\n/).filter((line) => line.trim()).slice(-20).join("\n");
|
|
782
|
+
const bounded = tailByUtf8Bytes(lines, 8 * 1024);
|
|
783
|
+
return bounded.trim() || undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Convert child session events into the private task-detail stream. */
|
|
787
|
+
export function forwardTelemetry(
|
|
788
|
+
event: unknown,
|
|
789
|
+
onTelemetry: (event: AgentTelemetryEvent) => void,
|
|
790
|
+
): void {
|
|
791
|
+
try {
|
|
792
|
+
const e = event as any;
|
|
793
|
+
switch (e?.type) {
|
|
794
|
+
case "turn_start":
|
|
795
|
+
safeEmitTelemetry(onTelemetry, {
|
|
796
|
+
kind: "turn_start",
|
|
797
|
+
turnIndex: finiteNumber(e.turnIndex),
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
case "message_update": {
|
|
801
|
+
const update = e.assistantMessageEvent;
|
|
802
|
+
if (update?.type === "text_delta" && typeof update.delta === "string") {
|
|
803
|
+
safeEmitTelemetry(onTelemetry, { kind: "text_delta", delta: update.delta });
|
|
804
|
+
} else if (update?.type === "thinking_start") {
|
|
805
|
+
safeEmitTelemetry(onTelemetry, { kind: "thinking_start" });
|
|
806
|
+
} else if (update?.type === "thinking_end") {
|
|
807
|
+
safeEmitTelemetry(onTelemetry, { kind: "thinking_end" });
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
case "message_end":
|
|
812
|
+
if (e.message?.role === "assistant") {
|
|
813
|
+
safeEmitTelemetry(onTelemetry, {
|
|
814
|
+
kind: "message_end",
|
|
815
|
+
text: assistantText(e.message),
|
|
816
|
+
usage: normalizeTurnUsage(e.message.usage),
|
|
817
|
+
error: typeof e.message.errorMessage === "string" ? e.message.errorMessage : undefined,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
case "tool_execution_start":
|
|
822
|
+
if (typeof e.toolName === "string") {
|
|
823
|
+
const args = toolArgsPreview(e.toolName, e.args);
|
|
824
|
+
safeEmitTelemetry(onTelemetry, {
|
|
825
|
+
kind: "tool_start",
|
|
826
|
+
toolCallId: typeof e.toolCallId === "string" ? e.toolCallId : `${e.toolName}:unknown`,
|
|
827
|
+
toolName: e.toolName,
|
|
828
|
+
...(args ? { toolArgs: args } : {}),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
return;
|
|
832
|
+
case "tool_execution_end":
|
|
833
|
+
safeEmitTelemetry(onTelemetry, {
|
|
834
|
+
kind: "tool_end",
|
|
835
|
+
toolCallId: typeof e.toolCallId === "string" ? e.toolCallId : `${String(e.toolName ?? "tool")}:unknown`,
|
|
836
|
+
toolName: typeof e.toolName === "string" ? e.toolName : "tool",
|
|
837
|
+
isError: Boolean(e.isError),
|
|
838
|
+
resultPreview: toolResultPreview(e.result),
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
case "auto_retry_start": {
|
|
842
|
+
const attempt = finiteNumber(e.attempt) ?? 0;
|
|
843
|
+
const maxAttempts = finiteNumber(e.maxAttempts) ?? 0;
|
|
844
|
+
const reason = typeof e.errorMessage === "string" ? safeDisplayText(e.errorMessage, 160) : "";
|
|
845
|
+
safeEmitTelemetry(onTelemetry, {
|
|
846
|
+
kind: "retry",
|
|
847
|
+
state: "start",
|
|
848
|
+
detail: `retry ${attempt}/${maxAttempts} in ${formatDelay(finiteNumber(e.delayMs) ?? 0)}${reason ? `: ${reason}` : ""}`,
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
case "auto_retry_end": {
|
|
853
|
+
const failure = typeof e.finalError === "string" ? safeDisplayText(e.finalError, 180) : "";
|
|
854
|
+
safeEmitTelemetry(onTelemetry, {
|
|
855
|
+
kind: "retry",
|
|
856
|
+
state: "end",
|
|
857
|
+
detail: e.success ? "retry succeeded" : failure ? `retry failed: ${failure}` : "retry failed",
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
case "compaction_start":
|
|
862
|
+
safeEmitTelemetry(onTelemetry, {
|
|
863
|
+
kind: "compaction",
|
|
864
|
+
state: "start",
|
|
865
|
+
detail: compactionStartDetail(e.reason),
|
|
866
|
+
});
|
|
867
|
+
return;
|
|
868
|
+
case "compaction_end":
|
|
869
|
+
safeEmitTelemetry(onTelemetry, {
|
|
870
|
+
kind: "compaction",
|
|
871
|
+
state: "end",
|
|
872
|
+
detail: e.aborted ? "compaction aborted" : e.errorMessage ? "compaction failed" : "compaction complete",
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
default:
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
} catch {
|
|
879
|
+
// best-effort: malformed provider events must not affect the run
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function readUsage(
|
|
884
|
+
session: any,
|
|
885
|
+
counters: {
|
|
886
|
+
retries?: number;
|
|
887
|
+
compactions?: number;
|
|
888
|
+
turns?: number;
|
|
889
|
+
toolUses?: number;
|
|
890
|
+
observing?: boolean;
|
|
891
|
+
} = {},
|
|
892
|
+
): AgentUsage {
|
|
433
893
|
try {
|
|
434
894
|
const stats = session.getSessionStats?.();
|
|
435
895
|
if (stats?.tokens) {
|
|
436
896
|
return {
|
|
897
|
+
inputTokens: stats.tokens.input ?? 0,
|
|
437
898
|
outputTokens: stats.tokens.output ?? 0,
|
|
438
|
-
|
|
899
|
+
cacheReadTokens: stats.tokens.cacheRead ?? 0,
|
|
900
|
+
cacheWriteTokens: stats.tokens.cacheWrite ?? 0,
|
|
901
|
+
totalTokens: (stats.tokens.input ?? 0) + (stats.tokens.output ?? 0),
|
|
439
902
|
cost: stats.cost ?? 0,
|
|
903
|
+
turns: counters.observing ? counters.turns ?? 0 : stats.assistantMessages ?? 0,
|
|
904
|
+
toolUses: counters.observing ? counters.toolUses ?? 0 : stats.toolCalls ?? 0,
|
|
905
|
+
retries: counters.retries ?? 0,
|
|
906
|
+
compactions: counters.compactions ?? 0,
|
|
440
907
|
};
|
|
441
908
|
}
|
|
442
909
|
} catch {
|
|
443
910
|
// fall through to message-based estimate
|
|
444
911
|
}
|
|
445
912
|
// Fallback: sum assistant usage from messages.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
let cost = 0;
|
|
449
|
-
for (const message of (session.messages ?? []) as Array<Partial<AssistantMessage>>) {
|
|
913
|
+
const usage = emptyAgentUsage();
|
|
914
|
+
for (const message of (session.messages ?? []) as Array<any>) {
|
|
450
915
|
if (message?.role === "assistant" && message.usage) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
916
|
+
usage.turns = (usage.turns ?? 0) + 1;
|
|
917
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + (message.usage.input ?? 0);
|
|
918
|
+
usage.outputTokens += message.usage.output ?? 0;
|
|
919
|
+
usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + (message.usage.cacheRead ?? 0);
|
|
920
|
+
usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + (message.usage.cacheWrite ?? 0);
|
|
921
|
+
usage.cost += message.usage.cost?.total ?? 0;
|
|
454
922
|
}
|
|
923
|
+
if (message?.role === "toolResult") usage.toolUses = (usage.toolUses ?? 0) + 1;
|
|
924
|
+
}
|
|
925
|
+
if (counters.observing) {
|
|
926
|
+
usage.turns = counters.turns ?? 0;
|
|
927
|
+
usage.toolUses = counters.toolUses ?? 0;
|
|
455
928
|
}
|
|
456
|
-
|
|
929
|
+
usage.totalTokens = (usage.inputTokens ?? 0) + usage.outputTokens;
|
|
930
|
+
usage.retries = counters.retries ?? 0;
|
|
931
|
+
usage.compactions = counters.compactions ?? 0;
|
|
932
|
+
return usage;
|
|
457
933
|
}
|
|
458
934
|
|
|
459
935
|
function lastAssistantText(messages: unknown[]): string {
|