@mono-agent/agent-runtime 0.14.0 → 0.15.1
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/MIGRATION.md +147 -69
- package/README.md +83 -20
- package/package.json +3 -6
- package/src/agent/compaction.js +0 -11
- package/src/agent/prompt/skill-index.js +5 -1
- package/src/ai/index.js +7 -1
- package/src/ai/observer.js +48 -13
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/providers/claude-cli.js +2 -13
- package/src/ai/providers/claude-sdk.js +14 -8
- package/src/ai/providers/codex-app.js +235 -56
- package/src/ai/providers/opencode-app.js +168 -3
- package/src/ai/providers/pi-messages.js +0 -8
- package/src/ai/providers/pi-native/compaction-driver.js +106 -10
- package/src/ai/providers/pi-native/result-builder.js +2 -14
- package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
- package/src/ai/providers/pi-native/turn-runner.js +31 -8
- package/src/ai/providers/pi-native.js +2 -5
- package/src/ai/runtime/live-input-events.js +94 -0
- package/src/ai/runtime/registry.js +8 -1
- package/src/ai/runtime/router.js +21 -0
- package/src/ai/types.js +2 -1
- package/src/runtime.js +3 -0
- package/types/agent/compaction.d.ts +0 -2
- package/types/agent/prompt/skill-index.d.ts +3 -0
- package/types/ai/index.d.ts +1 -1
- package/types/ai/observer.d.ts +4 -2
- package/types/ai/pi-interop.d.ts +113 -0
- package/types/ai/providers/claude-cli.d.ts +6 -30
- package/types/ai/providers/claude-sdk.d.ts +2 -9
- package/types/ai/providers/codex-app.d.ts +4 -10
- package/types/ai/providers/pi-messages.d.ts +0 -1
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
- package/types/ai/runtime/live-input-events.d.ts +22 -0
- package/types/ai/types.d.ts +10 -2
- package/src/ai/backend.js +0 -17
- package/src/ai/registry.js +0 -5
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
prepareCompaction,
|
|
34
34
|
shouldCompact,
|
|
35
35
|
} from "@earendil-works/pi-agent-core";
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
36
37
|
import {
|
|
37
38
|
estimateFixedOverheadTokens,
|
|
38
39
|
isLikelyContextTermination,
|
|
@@ -204,6 +205,52 @@ function previewCompactedContext(branchEntries, result) {
|
|
|
204
205
|
return estimateBuiltContextTokens([...branchEntries, previewEntry]);
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
function canonicalCompactionTrigger(trigger) {
|
|
209
|
+
return trigger === "reactive_overflow" ? "overflow" : trigger;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function finiteTokenCount(value) {
|
|
213
|
+
if (value === null || value === undefined) return undefined;
|
|
214
|
+
const count = Number(value);
|
|
215
|
+
return Number.isFinite(count) && count >= 0 ? count : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {((event: any) => void)|undefined} onEvent
|
|
220
|
+
* @param {{operationId: string, status: string, trigger: string, model?: string, tokensBefore?: number|null, tokensAfter?: number|null, reason?: string, message?: string}} event
|
|
221
|
+
*/
|
|
222
|
+
function emitCompactionEvent(onEvent, {
|
|
223
|
+
operationId,
|
|
224
|
+
status,
|
|
225
|
+
trigger,
|
|
226
|
+
model,
|
|
227
|
+
tokensBefore,
|
|
228
|
+
tokensAfter,
|
|
229
|
+
reason,
|
|
230
|
+
message,
|
|
231
|
+
}) {
|
|
232
|
+
const before = finiteTokenCount(tokensBefore);
|
|
233
|
+
const after = finiteTokenCount(tokensAfter);
|
|
234
|
+
try {
|
|
235
|
+
onEvent?.({
|
|
236
|
+
type: "context_compaction",
|
|
237
|
+
operationId,
|
|
238
|
+
status,
|
|
239
|
+
sdk: "pi",
|
|
240
|
+
trigger: canonicalCompactionTrigger(trigger),
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
...(model ? { model } : {}),
|
|
243
|
+
...(before === undefined ? {} : { tokensBefore: before }),
|
|
244
|
+
...(after === undefined ? {} : { tokensAfter: after }),
|
|
245
|
+
...(before === undefined && after === undefined ? {} : { tokenCountsExact: false }),
|
|
246
|
+
...(reason ? { reason } : {}),
|
|
247
|
+
...(message ? { message } : {}),
|
|
248
|
+
});
|
|
249
|
+
} catch {
|
|
250
|
+
// Observability must never change whether compaction itself succeeds.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
207
254
|
// Run a single guarded compaction. Requires the harness idle (callers
|
|
208
255
|
// waitForIdle first). Never throws — classifies AgentHarnessError into a warning
|
|
209
256
|
// and reports back whether anything was compacted. Fires onCompactionRecorded on
|
|
@@ -218,14 +265,22 @@ export async function tryCompact(harness, {
|
|
|
218
265
|
session,
|
|
219
266
|
policy,
|
|
220
267
|
}) {
|
|
221
|
-
const
|
|
222
|
-
|
|
268
|
+
const operationId = randomUUID();
|
|
269
|
+
emitCompactionEvent(onEvent, {
|
|
270
|
+
operationId,
|
|
271
|
+
status: "running",
|
|
272
|
+
trigger,
|
|
273
|
+
model,
|
|
223
274
|
});
|
|
224
|
-
|
|
275
|
+
let effectivePolicy = policy || {};
|
|
225
276
|
/** @type {null | {kind: string, tokensBefore?: number|null, tokensAfter?: number|null, savings?: number|null, error?: any}} */
|
|
226
277
|
let hookDecision = null;
|
|
227
278
|
let removeHook = null;
|
|
228
279
|
try {
|
|
280
|
+
const adaptivePolicy = resolveAgentCompactionPolicy({}, {
|
|
281
|
+
contextWindow: typeof harness?.getModel === "function" ? harness.getModel()?.contextWindow : undefined,
|
|
282
|
+
});
|
|
283
|
+
effectivePolicy = { ...adaptivePolicy, ...(policy || {}) };
|
|
229
284
|
if (typeof harness?.on !== "function") {
|
|
230
285
|
throw new Error("Pi AgentHarness does not expose session_before_compact hooks");
|
|
231
286
|
}
|
|
@@ -298,14 +353,13 @@ export async function tryCompact(harness, {
|
|
|
298
353
|
const reduced = measuredTokensBefore === null || tokensAfter === null
|
|
299
354
|
? null
|
|
300
355
|
: tokensAfter < measuredTokensBefore;
|
|
301
|
-
onEvent
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
source: "pi",
|
|
356
|
+
emitCompactionEvent(onEvent, {
|
|
357
|
+
operationId,
|
|
358
|
+
status: "succeeded",
|
|
305
359
|
trigger,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
360
|
+
model,
|
|
361
|
+
tokensBefore,
|
|
362
|
+
tokensAfter,
|
|
309
363
|
});
|
|
310
364
|
if (reduced === false) {
|
|
311
365
|
runtimeWarnings?.push({
|
|
@@ -354,6 +408,15 @@ export async function tryCompact(harness, {
|
|
|
354
408
|
? { minimum_savings_tokens: effectivePolicy.compactionMinSavingsTokens }
|
|
355
409
|
: {}),
|
|
356
410
|
});
|
|
411
|
+
emitCompactionEvent(onEvent, {
|
|
412
|
+
operationId,
|
|
413
|
+
status: "skipped",
|
|
414
|
+
trigger,
|
|
415
|
+
model,
|
|
416
|
+
tokensBefore: hookDecision.tokensBefore,
|
|
417
|
+
tokensAfter: hookDecision.tokensAfter,
|
|
418
|
+
reason: hookDecision.kind,
|
|
419
|
+
});
|
|
357
420
|
return {
|
|
358
421
|
applied: false,
|
|
359
422
|
tokensBefore: hookDecision.tokensBefore ?? null,
|
|
@@ -369,6 +432,13 @@ export async function tryCompact(harness, {
|
|
|
369
432
|
trigger,
|
|
370
433
|
message: "Nothing to compact",
|
|
371
434
|
});
|
|
435
|
+
emitCompactionEvent(onEvent, {
|
|
436
|
+
operationId,
|
|
437
|
+
status: "skipped",
|
|
438
|
+
trigger,
|
|
439
|
+
model,
|
|
440
|
+
reason: "nothing_to_compact",
|
|
441
|
+
});
|
|
372
442
|
return { applied: false, tokensBefore: null, tokensAfter: null, reduced: null, nothingToCompact: true };
|
|
373
443
|
}
|
|
374
444
|
const effectiveError = hookDecision?.kind === "failed" && hookDecision.error
|
|
@@ -385,6 +455,32 @@ export async function tryCompact(harness, {
|
|
|
385
455
|
? "context_compaction_busy"
|
|
386
456
|
: "context_compaction_failed";
|
|
387
457
|
runtimeWarnings?.push({ warning_kind: warningKind, source: "pi", trigger, message });
|
|
458
|
+
emitCompactionEvent(onEvent, {
|
|
459
|
+
operationId,
|
|
460
|
+
status: nothingToCompact ? "skipped" : "failed",
|
|
461
|
+
trigger,
|
|
462
|
+
model,
|
|
463
|
+
reason: nothingToCompact
|
|
464
|
+
? "nothing_to_compact"
|
|
465
|
+
: code === "auth"
|
|
466
|
+
? "authentication"
|
|
467
|
+
: code === "busy"
|
|
468
|
+
? "busy"
|
|
469
|
+
: code === "aborted"
|
|
470
|
+
? "cancelled"
|
|
471
|
+
: "provider_error",
|
|
472
|
+
...(nothingToCompact
|
|
473
|
+
? {}
|
|
474
|
+
: {
|
|
475
|
+
message: code === "auth"
|
|
476
|
+
? "Compaction authentication failed."
|
|
477
|
+
: code === "busy"
|
|
478
|
+
? "Context was busy and could not be compacted."
|
|
479
|
+
: code === "aborted"
|
|
480
|
+
? "Compaction was cancelled."
|
|
481
|
+
: "Compaction failed.",
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
388
484
|
return { applied: false, tokensBefore: null, tokensAfter: null, reduced: null, nothingToCompact };
|
|
389
485
|
} finally {
|
|
390
486
|
removeHook?.();
|
|
@@ -33,7 +33,7 @@ export function usageFromMessages(messages = []) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Normalize
|
|
36
|
+
* Normalize one provider request's usage into an exact context snapshot.
|
|
37
37
|
* Unlike usageFromMessages(), this deliberately does not aggregate earlier
|
|
38
38
|
* requests in the run: the last assistant usage is the same provider-counted
|
|
39
39
|
* value Pi's compaction logic trusts, so it can decrease after compaction.
|
|
@@ -74,15 +74,13 @@ export function failureKindForPiError(message, diagnostics, { maxTurnsHit = fals
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Emit the per-run cache / cost / provider-completed events.
|
|
77
|
-
* @param {{onEvent: (event: any) => void, resolved: any, reference: string, usage: {input: number, output: number, cacheRead: number, cacheWrite: number, cost: number},
|
|
77
|
+
* @param {{onEvent: (event: any) => void, resolved: any, reference: string, usage: {input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}, estimatedCost: number, start: number, externalAbort: boolean}} params
|
|
78
78
|
*/
|
|
79
79
|
export function emitUsageCostEvents({
|
|
80
80
|
onEvent,
|
|
81
81
|
resolved,
|
|
82
82
|
reference,
|
|
83
83
|
usage,
|
|
84
|
-
contextUsage,
|
|
85
|
-
contextWindow,
|
|
86
84
|
estimatedCost,
|
|
87
85
|
start,
|
|
88
86
|
externalAbort,
|
|
@@ -105,16 +103,6 @@ export function emitUsageCostEvents({
|
|
|
105
103
|
cacheCreationTokens: Number(usage.cacheWrite) || 0,
|
|
106
104
|
},
|
|
107
105
|
});
|
|
108
|
-
if (contextUsage) {
|
|
109
|
-
const effectiveContextWindow = Number(contextWindow) || 0;
|
|
110
|
-
onEvent({
|
|
111
|
-
type: "context_usage",
|
|
112
|
-
sdk: resolved.sdk,
|
|
113
|
-
model: reference,
|
|
114
|
-
...(effectiveContextWindow > 0 ? { contextWindow: effectiveContextWindow } : {}),
|
|
115
|
-
tokens: contextUsage,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
106
|
onEvent({
|
|
119
107
|
type: "provider_request_completed",
|
|
120
108
|
sdk: resolved.sdk,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
jsonSerializable,
|
|
14
14
|
streamContentKey,
|
|
15
15
|
} from "../pi-events.js";
|
|
16
|
+
import { contextUsageFromAssistantMessage } from "./result-builder.js";
|
|
16
17
|
|
|
17
18
|
function toolResultFileChange(result) {
|
|
18
19
|
const fileChange = result?.details?.file_change;
|
|
@@ -41,10 +42,10 @@ function toolResultFileChange(result) {
|
|
|
41
42
|
* abort; it is already constructed when this is wired (subscribe follows the
|
|
42
43
|
* AgentHarness constructor).
|
|
43
44
|
* @param {StreamSubscriberState} runState
|
|
44
|
-
* @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any}} deps
|
|
45
|
+
* @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any, sdk: string, model: string}} deps
|
|
45
46
|
* @returns {(event: any) => void}
|
|
46
47
|
*/
|
|
47
|
-
export function createStreamSubscriber(runState, { onEvent, options, toolLimits, harness }) {
|
|
48
|
+
export function createStreamSubscriber(runState, { onEvent, options, toolLimits, harness, sdk, model }) {
|
|
48
49
|
return (event) => {
|
|
49
50
|
if (event.type === "message_update") {
|
|
50
51
|
const streamEvent = event.assistantMessageEvent;
|
|
@@ -69,6 +70,23 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
69
70
|
onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
} else if (event.type === "message_end") {
|
|
74
|
+
const contextUsage = contextUsageFromAssistantMessage(event.message);
|
|
75
|
+
if (contextUsage) {
|
|
76
|
+
const contextWindow = Number(harness?.getModel?.()?.contextWindow) || 0;
|
|
77
|
+
const measurementId = typeof event.message?.id === "string" && event.message.id.trim().length > 0
|
|
78
|
+
? event.message.id
|
|
79
|
+
: undefined;
|
|
80
|
+
onEvent({
|
|
81
|
+
type: "context_usage",
|
|
82
|
+
sdk,
|
|
83
|
+
model,
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
...(measurementId === undefined ? {} : { measurementId }),
|
|
86
|
+
...(contextWindow > 0 ? { contextWindow } : {}),
|
|
87
|
+
tokens: contextUsage,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
72
90
|
} else if (event.type === "tool_execution_start") {
|
|
73
91
|
if (event.toolName) runState.lastToolName = event.toolName;
|
|
74
92
|
if (event.toolCallId) runState.toolStartTimes.set(event.toolCallId, Date.now());
|
|
@@ -205,6 +205,8 @@ export function buildTurnHarness(runState, {
|
|
|
205
205
|
onEvent,
|
|
206
206
|
options,
|
|
207
207
|
toolLimits,
|
|
208
|
+
sdk,
|
|
209
|
+
reference,
|
|
208
210
|
}) {
|
|
209
211
|
const harness = new AgentHarness({
|
|
210
212
|
env: new NodeExecutionEnv({ cwd: cwd || process.cwd() }),
|
|
@@ -228,7 +230,14 @@ export function buildTurnHarness(runState, {
|
|
|
228
230
|
: undefined);
|
|
229
231
|
runState.harness = harness;
|
|
230
232
|
|
|
231
|
-
harness.subscribe(createStreamSubscriber(runState, {
|
|
233
|
+
harness.subscribe(createStreamSubscriber(runState, {
|
|
234
|
+
onEvent,
|
|
235
|
+
options,
|
|
236
|
+
toolLimits,
|
|
237
|
+
harness,
|
|
238
|
+
sdk,
|
|
239
|
+
model: reference,
|
|
240
|
+
}));
|
|
232
241
|
|
|
233
242
|
const abortHandler = () => {
|
|
234
243
|
runState.externalAbort = true;
|
|
@@ -255,12 +264,24 @@ export function startLiveInput({ harness, options, onEvent }) {
|
|
|
255
264
|
? options.liveInput[Symbol.asyncIterator]()
|
|
256
265
|
: options.liveInput;
|
|
257
266
|
let runComplete = false;
|
|
267
|
+
/** @type {() => void} */
|
|
268
|
+
let signalStop = () => {};
|
|
269
|
+
const stopped = new Promise((resolve) => { signalStop = () => resolve(); });
|
|
258
270
|
const task = (async () => {
|
|
259
271
|
try {
|
|
260
272
|
while (!runComplete && !options.abortSignal?.aborted) {
|
|
261
|
-
const next = await
|
|
273
|
+
const next = await Promise.race([
|
|
274
|
+
iterator.next(),
|
|
275
|
+
stopped.then(() => ({ done: true, value: undefined })),
|
|
276
|
+
]);
|
|
262
277
|
if (next.done || runComplete || options.abortSignal?.aborted) break;
|
|
263
|
-
|
|
278
|
+
try {
|
|
279
|
+
await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
|
|
280
|
+
next.value.acknowledge?.();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
next.value.reject?.(err);
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
264
285
|
}
|
|
265
286
|
} catch (err) {
|
|
266
287
|
onEvent({
|
|
@@ -273,15 +294,17 @@ export function startLiveInput({ harness, options, onEvent }) {
|
|
|
273
294
|
return {
|
|
274
295
|
// The run is done: stop the live-steering consumer so it cannot steer a
|
|
275
296
|
// finished harness or swallow a follow-up meant for the next turn. We signal
|
|
276
|
-
// completion, then best-effort return() the iterator
|
|
277
|
-
//
|
|
278
|
-
//
|
|
297
|
+
// completion, then best-effort return() the iterator. The explicit stop
|
|
298
|
+
// race releases the task even when a third-party iterator's return() does
|
|
299
|
+
// not unblock its pending next(); awaiting the task still closes any steer
|
|
300
|
+
// acknowledgement already in progress.
|
|
279
301
|
stop: async () => {
|
|
280
302
|
runComplete = true;
|
|
303
|
+
signalStop();
|
|
281
304
|
if (iterator && typeof iterator.return === "function") {
|
|
282
|
-
try {
|
|
305
|
+
try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
|
|
283
306
|
}
|
|
284
|
-
|
|
307
|
+
await task;
|
|
285
308
|
},
|
|
286
309
|
};
|
|
287
310
|
}
|
|
@@ -52,7 +52,6 @@ import {
|
|
|
52
52
|
buildErrorDetails,
|
|
53
53
|
buildErrorResult,
|
|
54
54
|
buildSuccessResult,
|
|
55
|
-
contextUsageFromAssistantMessage,
|
|
56
55
|
emitCapabilitiesResolved,
|
|
57
56
|
emitUsageCostEvents,
|
|
58
57
|
usageFromMessages,
|
|
@@ -453,6 +452,8 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
453
452
|
onEvent,
|
|
454
453
|
options,
|
|
455
454
|
toolLimits,
|
|
455
|
+
sdk: resolved.sdk,
|
|
456
|
+
reference,
|
|
456
457
|
});
|
|
457
458
|
|
|
458
459
|
// Seed prior transcript (everything before the trailing user turn) into the
|
|
@@ -614,10 +615,6 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
614
615
|
resolved,
|
|
615
616
|
reference,
|
|
616
617
|
usage,
|
|
617
|
-
contextUsage: runState.externalAbort || runState.maxTurnsHit || runError
|
|
618
|
-
? null
|
|
619
|
-
: contextUsageFromAssistantMessage(lastAssistant),
|
|
620
|
-
contextWindow: runState.compaction.policy?.contextWindow,
|
|
621
618
|
estimatedCost,
|
|
622
619
|
start,
|
|
623
620
|
externalAbort: runState.externalAbort,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Metadata-only live-input acknowledgement instrumentation.
|
|
2
|
+
//
|
|
3
|
+
// Provider bridges already call message.acknowledge() only after their native
|
|
4
|
+
// steering boundary accepts guidance. Wrapping that callback here creates one
|
|
5
|
+
// adapter-neutral `live_input_applied` event without copying the guidance body
|
|
6
|
+
// into runtime telemetry. A wrapper owns one logical-run dedupe set and is
|
|
7
|
+
// intentionally reused by the fallback router across provider attempts.
|
|
8
|
+
|
|
9
|
+
// @ts-check
|
|
10
|
+
|
|
11
|
+
const LIVE_INPUT_APPLIED_INSTRUMENTED = Symbol("mono-agent.live-input-applied-instrumented");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (reason?: unknown) => void}} RuntimeLiveInputMessage
|
|
15
|
+
* @typedef {{type: "live_input_applied", inputId: string, receivedAt?: string}} LiveInputAppliedEvent
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
|
|
20
|
+
* @param {(event: LiveInputAppliedEvent) => void} onApplied
|
|
21
|
+
* @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
|
|
22
|
+
*/
|
|
23
|
+
export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
|
|
24
|
+
if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
|
|
25
|
+
|
|
26
|
+
const appliedInputIds = new Set();
|
|
27
|
+
const instrumented = {
|
|
28
|
+
[LIVE_INPUT_APPLIED_INSTRUMENTED]: true,
|
|
29
|
+
[Symbol.asyncIterator]() {
|
|
30
|
+
const iterator = liveInput[Symbol.asyncIterator]();
|
|
31
|
+
let ordinal = 0;
|
|
32
|
+
return {
|
|
33
|
+
async next() {
|
|
34
|
+
const next = await iterator.next();
|
|
35
|
+
if (next.done === true) return next;
|
|
36
|
+
ordinal += 1;
|
|
37
|
+
const message = next.value;
|
|
38
|
+
const inputId = stableInputId(message?.id, ordinal);
|
|
39
|
+
const receivedAt = typeof message?.receivedAt === "string" && message.receivedAt.length > 0
|
|
40
|
+
? message.receivedAt
|
|
41
|
+
: undefined;
|
|
42
|
+
const acknowledge = typeof message?.acknowledge === "function"
|
|
43
|
+
? message.acknowledge.bind(message)
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
done: false,
|
|
47
|
+
value: {
|
|
48
|
+
...message,
|
|
49
|
+
acknowledge: () => {
|
|
50
|
+
acknowledge?.();
|
|
51
|
+
if (appliedInputIds.has(inputId)) return;
|
|
52
|
+
appliedInputIds.add(inputId);
|
|
53
|
+
try {
|
|
54
|
+
onApplied({
|
|
55
|
+
type: "live_input_applied",
|
|
56
|
+
inputId,
|
|
57
|
+
...(receivedAt === undefined ? {} : { receivedAt }),
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// Telemetry must never turn accepted guidance into a provider
|
|
61
|
+
// failure after the native steering call already succeeded.
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async return(value) {
|
|
68
|
+
return typeof iterator.return === "function"
|
|
69
|
+
? iterator.return(value)
|
|
70
|
+
: { done: true, value };
|
|
71
|
+
},
|
|
72
|
+
async throw(error) {
|
|
73
|
+
if (typeof iterator.throw === "function") return iterator.throw(error);
|
|
74
|
+
throw error;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {unknown} value */
|
|
83
|
+
function isInstrumented(value) {
|
|
84
|
+
return typeof value === "object"
|
|
85
|
+
&& value !== null
|
|
86
|
+
&& value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {unknown} value @param {number} ordinal */
|
|
90
|
+
function stableInputId(value, ordinal) {
|
|
91
|
+
return typeof value === "string" && value.trim().length > 0
|
|
92
|
+
? value
|
|
93
|
+
: `anonymous:${ordinal}`;
|
|
94
|
+
}
|
|
@@ -29,7 +29,14 @@ const builtinBridgeSpecs = {
|
|
|
29
29
|
id: "claude-code",
|
|
30
30
|
supports: (ref, options) => ref?.sdk === "claude" && options?.executionMode === "cli",
|
|
31
31
|
// The claude CLI resumes prior sessions via `--resume <sessionId>`.
|
|
32
|
-
capabilities: () => ({
|
|
32
|
+
capabilities: () => ({
|
|
33
|
+
kind: "claude-code",
|
|
34
|
+
runtime: "cli",
|
|
35
|
+
...COMMON_CAPABILITIES,
|
|
36
|
+
supports_session_resume: true,
|
|
37
|
+
// The one-shot CLI bridge has no bidirectional stdin steering channel.
|
|
38
|
+
supports_live_input: false,
|
|
39
|
+
}),
|
|
33
40
|
load: async () => (await import("../providers/claude-cli.js")).claudeCodeRuntimeBridge,
|
|
34
41
|
},
|
|
35
42
|
"codex-app": {
|
package/src/ai/runtime/router.js
CHANGED
|
@@ -41,6 +41,8 @@ import { runtimeCapabilities } from "./capabilities.js";
|
|
|
41
41
|
import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
|
|
42
42
|
import { passthroughSandbox } from "../../agent/sandbox-seam.js";
|
|
43
43
|
import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
44
|
+
import { createObserverHub } from "../observer.js";
|
|
45
|
+
import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
|
|
@@ -127,6 +129,22 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
127
129
|
* @returns {Promise<RuntimeResult>}
|
|
128
130
|
*/
|
|
129
131
|
async run(systemPrompt, options = {}) {
|
|
132
|
+
const liveInputHub = options.liveInput === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: createObserverHub({
|
|
135
|
+
observers: [
|
|
136
|
+
...(Array.isArray(host.observers) ? host.observers : []),
|
|
137
|
+
...(Array.isArray(options.observers) ? options.observers : []),
|
|
138
|
+
],
|
|
139
|
+
onEvent: options.onEvent,
|
|
140
|
+
});
|
|
141
|
+
if (options.liveInput !== undefined && liveInputHub !== undefined) {
|
|
142
|
+
options = {
|
|
143
|
+
...options,
|
|
144
|
+
liveInput: instrumentLiveInputAppliedEvents(options.liveInput, liveInputHub.emit),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
130
148
|
/** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null), routeSafety?: import('../types.js').RuntimeRouteSafetyMode, safetyContract?: import('../types.js').RuntimeRouteSafetyContract}>} */
|
|
131
149
|
const failoverHistory = [];
|
|
132
150
|
/** @type {RuntimeResult|null} */
|
|
@@ -374,6 +392,9 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
374
392
|
failoverHistory,
|
|
375
393
|
routeSafetyHistory,
|
|
376
394
|
};
|
|
395
|
+
} finally {
|
|
396
|
+
await liveInputHub?.flush();
|
|
397
|
+
}
|
|
377
398
|
},
|
|
378
399
|
chain: () => entries.slice(),
|
|
379
400
|
configureTools(next = {}) {
|
package/src/ai/types.js
CHANGED
|
@@ -131,9 +131,10 @@
|
|
|
131
131
|
* @property {string} [executionMode] "sdk" (default) or "cli"; selects which bridge variant handles the model.
|
|
132
132
|
* @property {string} [sessionId] Host conversation/session key for resumable bridges.
|
|
133
133
|
* @property {string} [providerSessionId] Provider-owned resume id for resumable bridges.
|
|
134
|
+
* @property {typeof import("@anthropic-ai/claude-agent-sdk").query} [claudeAgentQuery] Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
|
|
134
135
|
* @property {boolean} [sessionKeepAlive] Keep resumable provider state alive after the turn.
|
|
135
136
|
* @property {number} [sessionIdleTimeoutMs] Idle TTL for resumable provider state.
|
|
136
|
-
* @property {AsyncIterable<{body: string, id?: string}>} [liveInput] Stream of in-flight user messages for steering an active run.
|
|
137
|
+
* @property {AsyncIterable<{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (error?: unknown) => void}>} [liveInput] Stream of in-flight user messages for steering an active run. Providers acknowledge only after accepting a message into the active turn.
|
|
137
138
|
* @property {ReadonlyArray<*>} [observers] Per-call observers (see RuntimeObserver) merged with host-level (createRuntime) observers.
|
|
138
139
|
* @property {(event: RuntimeEvent) => void} [onEvent]
|
|
139
140
|
* @property {ReadonlyArray<Object>} [messages]
|
package/src/runtime.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { createToolContext, updateToolContext } from "./agent/tools/shared/tool-context.js";
|
|
41
41
|
import { resolveRuntimeBrand } from "./runtime-brand.js";
|
|
42
42
|
import { retireDurableNativeSession } from "./ai/providers/pi-native/session-lifecycle.js";
|
|
43
|
+
import { instrumentLiveInputAppliedEvents } from "./ai/runtime/live-input-events.js";
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* @typedef {import('./ai/types.js').AgentRuntimeHostOptions} AgentRuntimeHostOptions
|
|
@@ -159,6 +160,7 @@ export function createRuntime(host = {}) {
|
|
|
159
160
|
observers: [...hostObservers, ...callObservers],
|
|
160
161
|
onEvent: options.onEvent,
|
|
161
162
|
});
|
|
163
|
+
const liveInput = instrumentLiveInputAppliedEvents(options.liveInput, hub.emit);
|
|
162
164
|
const prompts = resolvePrompts(host.prompts, options.prompts);
|
|
163
165
|
const result = await bridge.execute(systemPrompt, {
|
|
164
166
|
...hostDefaults,
|
|
@@ -172,6 +174,7 @@ export function createRuntime(host = {}) {
|
|
|
172
174
|
toolContext,
|
|
173
175
|
observerHub: hub,
|
|
174
176
|
onEvent: hub.emit,
|
|
177
|
+
...(liveInput === undefined ? {} : { liveInput }),
|
|
175
178
|
// Merged AFTER the spreads so the per-field run>host>default precedence
|
|
176
179
|
// wins over either bag's whole-object `prompts`.
|
|
177
180
|
...(prompts === undefined ? {} : { prompts }),
|
|
@@ -84,8 +84,6 @@ export type AgentCompactionPolicy = {
|
|
|
84
84
|
summaryMaxTokens: number;
|
|
85
85
|
fixedOverheadEnabled: boolean;
|
|
86
86
|
compactionMinSavingsTokens: number;
|
|
87
|
-
toolPayloadCompactionTriggerChars: number;
|
|
88
|
-
toolPruneTriggerTokens: number;
|
|
89
87
|
toolTextLimitChars: number;
|
|
90
88
|
bashOutputLimitChars: number;
|
|
91
89
|
mcpTextLimitChars: number;
|
|
@@ -8,6 +8,9 @@ export function buildSkillPathNote({ assetsPath, skillsRoot }?: {
|
|
|
8
8
|
skillsRoot?: any;
|
|
9
9
|
}): string;
|
|
10
10
|
/**
|
|
11
|
+
* Render a complete skill body plus its path note. Omitting `maxChars` returns
|
|
12
|
+
* the full text; pass a positive `maxChars` only when explicit truncation is
|
|
13
|
+
* required by the caller.
|
|
11
14
|
* @param {{body?: string, assetsPath?: string, skillsRoot?: any, maxChars?: number}} [options]
|
|
12
15
|
*/
|
|
13
16
|
export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxChars }?: {
|
package/types/ai/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export * from "./registry.js";
|
|
2
1
|
export * from "./runtime/model-refs.js";
|
|
3
2
|
export * from "./runtime/registry.js";
|
|
4
3
|
export { createSessionRegistry, disposeAllProviderSessions, disposeProviderSession, invalidateProviderSession, refreshProviderSession, syncProviderSession } from "./runtime/sessions.js";
|
|
5
4
|
export { createMetricsObserver, createObserverHub } from "./observer.js";
|
|
6
5
|
export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
|
|
6
|
+
export { getPiBuiltinModel, listPiBuiltinModels, loginPiOAuth, reasoningLevelsForPiModel, resolvePiOAuthApiKey } from "./pi-interop.js";
|
|
7
7
|
export { CLAUDE_SDK_CATALOG_VERSION, createClaudeSdkDiscoveryIsolation, curatedClaudeSdkModels, discoverClaudeSdkModels, normalizeClaudeSdkCatalog, normalizeClaudeSdkModelId } from "./providers/claude-sdk-discovery.js";
|
|
8
8
|
export { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings, UNKNOWN_CAPABILITY } from "./runtime/capabilities-used.js";
|
package/types/ai/observer.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ export function createObserverHub({ observers, onEvent }?: {
|
|
|
14
14
|
flush: () => Promise<void>;
|
|
15
15
|
observers: () => any[];
|
|
16
16
|
};
|
|
17
|
-
export function createMetricsObserver({ name }?: {
|
|
17
|
+
export function createMetricsObserver({ name, maxLatencySamples }?: {
|
|
18
18
|
name?: string;
|
|
19
|
+
maxLatencySamples?: number;
|
|
19
20
|
}): {
|
|
20
21
|
name: string;
|
|
21
22
|
recordEvent: (event: any) => void;
|
|
@@ -49,7 +50,8 @@ export function createMetricsObserver({ name }?: {
|
|
|
49
50
|
byKind: any;
|
|
50
51
|
};
|
|
51
52
|
turns: {
|
|
52
|
-
count:
|
|
53
|
+
count: any;
|
|
54
|
+
sampleCount: number;
|
|
53
55
|
latencyMsP50: any;
|
|
54
56
|
latencyMsP95: any;
|
|
55
57
|
};
|