@mono-agent/agent-runtime 0.20.14 → 0.21.0
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/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +30 -7
- package/README.md +219 -35
- package/package.json +9 -4
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +104 -5
- package/src/agent/tools/bash.js +10 -2
- package/src/agent/tools/codex-subscription-search.js +122 -28
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/monitor.js +11 -2
- package/src/agent/tools/pi-bridge.js +33 -14
- package/src/agent/tools/shared/monitors.js +22 -3
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +3 -1
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +1 -0
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/pi-native/compaction-driver.js +45 -21
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +40 -2
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +28 -4
- package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
- package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +245 -13
- package/src/ai/providers/pi-native.js +159 -40
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/router.js +30 -11
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +26 -5
- package/src/runtime.js +24 -5
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -1
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +6 -2
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/monitor.d.ts +5 -2
- package/types/agent/tools/pi-bridge.d.ts +6 -4
- package/types/agent/tools/shared/monitors.d.ts +17 -2
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +3 -2
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/index.d.ts +1 -0
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +61 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +140 -12
|
@@ -1,68 +1,102 @@
|
|
|
1
|
-
// Metadata-only live-input
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// intentionally reused by the fallback router across provider attempts.
|
|
1
|
+
// Metadata-only live-input lifecycle instrumentation and logical-run replay fencing.
|
|
2
|
+
// Stable ids identify one logical delivery across provider retries. The first
|
|
3
|
+
// occurrence owns the body and identity; later same-id occurrences are invalid
|
|
4
|
+
// duplicates unless an exact opaque logical-owner token proves they carry a new
|
|
5
|
+
// callback lease for that first owner. Anonymous input remains legal but is
|
|
6
|
+
// never replayed.
|
|
8
7
|
|
|
9
8
|
// @ts-check
|
|
10
9
|
|
|
11
|
-
const
|
|
10
|
+
const LIVE_INPUT_INSTRUMENTED = Symbol("mono-agent.live-input-instrumented");
|
|
11
|
+
const MAX_DIAGNOSTICS_PER_KIND = 100;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @typedef {{
|
|
15
|
-
* @typedef {{
|
|
14
|
+
* @typedef {{providerEntryId?: string, providerRunId?: string}} RuntimeLiveInputEvidence
|
|
15
|
+
* @typedef {{reason: "delivery_uncertain", providerEntryId?: string, providerRunId?: string}} RuntimeLiveInputUncertainty
|
|
16
|
+
* @typedef {{body: string, id?: string, receivedAt?: string, logicalOwner?: object, accepted?: (evidence?: RuntimeLiveInputEvidence) => unknown, acknowledge?: (evidence?: RuntimeLiveInputEvidence) => unknown, uncertain?: (details: RuntimeLiveInputUncertainty) => unknown, reject?: (reason?: unknown) => unknown}} RuntimeLiveInputMessage
|
|
17
|
+
* @typedef {{message: RuntimeLiveInputMessage, callbackMessage: RuntimeLiveInputMessage, logicalOwner?: object, inputId: string, receivedAt?: string, phase: "available"|"leased"|"native_accepted"|"consumed"|"uncertain", attempt: number, generation: number}} LiveInputOwner
|
|
18
|
+
* @typedef {{type: string, [key: string]: unknown}} LiveInputEvent
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
|
|
20
|
-
* @param {(event:
|
|
23
|
+
* @param {(event: LiveInputEvent) => void} onEvent
|
|
21
24
|
* @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
|
|
22
25
|
*/
|
|
23
|
-
export function instrumentLiveInputAppliedEvents(liveInput,
|
|
26
|
+
export function instrumentLiveInputAppliedEvents(liveInput, onEvent) {
|
|
24
27
|
if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
/** @type {Map<string, LiveInputOwner>} */
|
|
30
|
+
const owners = new Map();
|
|
31
|
+
const diagnosticCounts = new Map();
|
|
32
|
+
let generation = 0;
|
|
33
|
+
|
|
27
34
|
const instrumented = {
|
|
28
|
-
[
|
|
35
|
+
[LIVE_INPUT_INSTRUMENTED]: true,
|
|
29
36
|
[Symbol.asyncIterator]() {
|
|
37
|
+
generation += 1;
|
|
38
|
+
const iteratorGeneration = generation;
|
|
30
39
|
const iterator = liveInput[Symbol.asyncIterator]();
|
|
40
|
+
const yieldedIds = new Set();
|
|
31
41
|
let ordinal = 0;
|
|
42
|
+
|
|
32
43
|
return {
|
|
33
44
|
async next() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
45
|
+
while (true) {
|
|
46
|
+
const next = await iterator.next();
|
|
47
|
+
if (next.done === true) return next;
|
|
48
|
+
ordinal += 1;
|
|
49
|
+
const message = next.value;
|
|
50
|
+
const stableId = normalizedStableId(message?.id);
|
|
51
|
+
|
|
52
|
+
if (stableId === undefined) {
|
|
53
|
+
if (iteratorGeneration > 1) {
|
|
54
|
+
emitBounded("anonymous_identity", {
|
|
55
|
+
type: "live_input_replay_suppressed",
|
|
56
|
+
reason: "anonymous_identity",
|
|
57
|
+
generation: iteratorGeneration,
|
|
58
|
+
ordinal,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const owner = createOwner(message, `anonymous:${iteratorGeneration}:${ordinal}`, iteratorGeneration);
|
|
63
|
+
return { done: false, value: lease(owner) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const existing = owners.get(stableId);
|
|
67
|
+
if (existing === undefined) {
|
|
68
|
+
const owner = createOwner(message, stableId, iteratorGeneration);
|
|
69
|
+
owners.set(stableId, owner);
|
|
70
|
+
yieldedIds.add(stableId);
|
|
71
|
+
return { done: false, value: lease(owner) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
emitBounded("duplicate_id", {
|
|
75
|
+
type: "live_input_duplicate_suppressed",
|
|
76
|
+
inputId: stableId,
|
|
77
|
+
generation: iteratorGeneration,
|
|
78
|
+
});
|
|
79
|
+
if (
|
|
80
|
+
iteratorGeneration > existing.generation
|
|
81
|
+
&& existing.phase === "available"
|
|
82
|
+
&& !yieldedIds.has(stableId)
|
|
83
|
+
) {
|
|
84
|
+
// Suppress the later occurrence itself. It merely reveals that the
|
|
85
|
+
// first owner is present in this replay generation; replay the
|
|
86
|
+
// first owner's immutable body and identity.
|
|
87
|
+
yieldedIds.add(stableId);
|
|
88
|
+
existing.generation = iteratorGeneration;
|
|
89
|
+
if (
|
|
90
|
+
existing.logicalOwner !== undefined
|
|
91
|
+
&& existing.logicalOwner === validLogicalOwner(message?.logicalOwner)
|
|
92
|
+
) {
|
|
93
|
+
// The logical owner remains the first occurrence. This is only
|
|
94
|
+
// its fresh, independently fenced host callback lease.
|
|
95
|
+
existing.callbackMessage = message;
|
|
96
|
+
}
|
|
97
|
+
return { done: false, value: lease(existing) };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
66
100
|
},
|
|
67
101
|
async return(value) {
|
|
68
102
|
return typeof iterator.return === "function"
|
|
@@ -76,19 +110,181 @@ export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
|
|
|
76
110
|
};
|
|
77
111
|
},
|
|
78
112
|
};
|
|
113
|
+
|
|
79
114
|
return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
|
|
115
|
+
|
|
116
|
+
/** @param {RuntimeLiveInputMessage} message @param {string} inputId @param {number} ownerGeneration */
|
|
117
|
+
function createOwner(message, inputId, ownerGeneration) {
|
|
118
|
+
const receivedAt = validString(message?.receivedAt);
|
|
119
|
+
const logicalOwner = validLogicalOwner(message?.logicalOwner);
|
|
120
|
+
return {
|
|
121
|
+
message,
|
|
122
|
+
callbackMessage: message,
|
|
123
|
+
...(logicalOwner === undefined ? {} : { logicalOwner }),
|
|
124
|
+
inputId,
|
|
125
|
+
...(receivedAt === undefined ? {} : { receivedAt }),
|
|
126
|
+
phase: /** @type {const} */ ("available"),
|
|
127
|
+
attempt: 0,
|
|
128
|
+
generation: ownerGeneration,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {LiveInputOwner} owner */
|
|
133
|
+
function lease(owner) {
|
|
134
|
+
owner.phase = "leased";
|
|
135
|
+
owner.attempt += 1;
|
|
136
|
+
const attempt = owner.attempt;
|
|
137
|
+
const host = owner.callbackMessage;
|
|
138
|
+
const base = eventBase(owner);
|
|
139
|
+
|
|
140
|
+
/** @type {RuntimeLiveInputMessage} */
|
|
141
|
+
const wrapped = {
|
|
142
|
+
...owner.message,
|
|
143
|
+
accepted(evidence) {
|
|
144
|
+
if (!isCurrent(owner, attempt, "leased")) return "ignored";
|
|
145
|
+
owner.phase = "native_accepted";
|
|
146
|
+
const disposition = callHost(host.accepted, host, evidence);
|
|
147
|
+
emit({
|
|
148
|
+
type: "live_input_native_accepted",
|
|
149
|
+
...base,
|
|
150
|
+
...safeEvidence(evidence),
|
|
151
|
+
settlementDisposition: disposition,
|
|
152
|
+
});
|
|
153
|
+
return publicDisposition(disposition);
|
|
154
|
+
},
|
|
155
|
+
acknowledge(evidence) {
|
|
156
|
+
if (!isCurrent(owner, attempt, "leased", "native_accepted")) {
|
|
157
|
+
emit({ type: "live_input_consumed", ...base, ...safeEvidence(evidence), late: true });
|
|
158
|
+
return "ignored";
|
|
159
|
+
}
|
|
160
|
+
owner.phase = "consumed";
|
|
161
|
+
emit({ type: "live_input_consumed", ...base, ...safeEvidence(evidence) });
|
|
162
|
+
const disposition = callHost(host.acknowledge, host, evidence);
|
|
163
|
+
if (disposition === "recorded") {
|
|
164
|
+
emit({ type: "live_input_applied", ...base, ...safeEvidence(evidence) });
|
|
165
|
+
} else {
|
|
166
|
+
emit({
|
|
167
|
+
type: "live_input_settlement_unconfirmed",
|
|
168
|
+
...base,
|
|
169
|
+
phase: "consumed",
|
|
170
|
+
settlementDisposition: disposition,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return publicDisposition(disposition);
|
|
174
|
+
},
|
|
175
|
+
uncertain(details) {
|
|
176
|
+
if (!isCurrent(owner, attempt, "leased", "native_accepted")) return "ignored";
|
|
177
|
+
owner.phase = "uncertain";
|
|
178
|
+
const normalized = {
|
|
179
|
+
reason: /** @type {const} */ ("delivery_uncertain"),
|
|
180
|
+
...safeEvidence(details),
|
|
181
|
+
};
|
|
182
|
+
const disposition = callHost(host.uncertain, host, normalized);
|
|
183
|
+
emit({
|
|
184
|
+
type: "live_input_uncertain",
|
|
185
|
+
...base,
|
|
186
|
+
...normalized,
|
|
187
|
+
settlementDisposition: disposition,
|
|
188
|
+
});
|
|
189
|
+
return publicDisposition(disposition);
|
|
190
|
+
},
|
|
191
|
+
reject(reason) {
|
|
192
|
+
if (!isCurrent(owner, attempt, "leased", "native_accepted")) return "ignored";
|
|
193
|
+
if (owner.phase === "native_accepted" && !isNativeQueueRemoved(reason)) {
|
|
194
|
+
return wrapped.uncertain?.({ reason: "delivery_uncertain" });
|
|
195
|
+
}
|
|
196
|
+
const disposition = callHost(host.reject, host, reason);
|
|
197
|
+
// "ignored" is affirmative host evidence that this attempt no longer
|
|
198
|
+
// owns settlement (normally because the mailbox already sealed it).
|
|
199
|
+
// Legacy void and thrown callbacks remain replay-compatible because
|
|
200
|
+
// native removal still proves the provider did not consume the input.
|
|
201
|
+
owner.phase = disposition === "ignored" ? "uncertain" : "available";
|
|
202
|
+
return publicDisposition(disposition);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
return wrapped;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @param {string} kind @param {LiveInputEvent} event */
|
|
209
|
+
function emitBounded(kind, event) {
|
|
210
|
+
const count = diagnosticCounts.get(kind) ?? 0;
|
|
211
|
+
if (count >= MAX_DIAGNOSTICS_PER_KIND) return;
|
|
212
|
+
diagnosticCounts.set(kind, count + 1);
|
|
213
|
+
emit(event);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @param {LiveInputEvent} event */
|
|
217
|
+
function emit(event) {
|
|
218
|
+
try { onEvent(event); } catch { /* telemetry never changes delivery */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @param {unknown} callback @param {RuntimeLiveInputMessage} receiver @param {unknown} argument */
|
|
223
|
+
function callHost(callback, receiver, argument) {
|
|
224
|
+
if (typeof callback !== "function") return "unconfirmed";
|
|
225
|
+
try {
|
|
226
|
+
const result = callback.call(receiver, argument);
|
|
227
|
+
return result === "recorded" || result === "ignored" ? result : "unconfirmed";
|
|
228
|
+
} catch {
|
|
229
|
+
return "threw";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** @param {string} disposition */
|
|
234
|
+
function publicDisposition(disposition) {
|
|
235
|
+
return disposition === "recorded" || disposition === "ignored" ? disposition : undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @param {LiveInputOwner} owner */
|
|
239
|
+
function eventBase(owner) {
|
|
240
|
+
return {
|
|
241
|
+
inputId: owner.inputId,
|
|
242
|
+
...(owner.receivedAt === undefined ? {} : { receivedAt: owner.receivedAt }),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {LiveInputOwner} owner @param {number} attempt @param {...string} phases */
|
|
247
|
+
function isCurrent(owner, attempt, ...phases) {
|
|
248
|
+
return owner.attempt === attempt && phases.includes(owner.phase);
|
|
80
249
|
}
|
|
81
250
|
|
|
82
251
|
/** @param {unknown} value */
|
|
83
|
-
function
|
|
84
|
-
return typeof value === "
|
|
85
|
-
&& value !== null
|
|
86
|
-
&& value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
|
|
252
|
+
function normalizedStableId(value) {
|
|
253
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
87
254
|
}
|
|
88
255
|
|
|
89
|
-
/** @param {unknown} value
|
|
90
|
-
function
|
|
91
|
-
return typeof value === "
|
|
256
|
+
/** @param {unknown} value */
|
|
257
|
+
function validLogicalOwner(value) {
|
|
258
|
+
return value !== null && (typeof value === "object" || typeof value === "function")
|
|
92
259
|
? value
|
|
93
|
-
:
|
|
260
|
+
: undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** @param {unknown} value */
|
|
264
|
+
function validString(value) {
|
|
265
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @param {unknown} value */
|
|
269
|
+
function safeEvidence(value) {
|
|
270
|
+
if (typeof value !== "object" || value === null) return {};
|
|
271
|
+
const evidence = /** @type {{providerEntryId?: unknown, providerRunId?: unknown}} */ (value);
|
|
272
|
+
return {
|
|
273
|
+
...(validString(evidence.providerEntryId) === undefined ? {} : { providerEntryId: evidence.providerEntryId }),
|
|
274
|
+
...(validString(evidence.providerRunId) === undefined ? {} : { providerRunId: evidence.providerRunId }),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** @param {unknown} reason */
|
|
279
|
+
function isNativeQueueRemoved(reason) {
|
|
280
|
+
return typeof reason === "object"
|
|
281
|
+
&& reason !== null
|
|
282
|
+
&& /** @type {{code?: unknown}} */ (reason).code === "native_queue_removed";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @param {unknown} value */
|
|
286
|
+
function isInstrumented(value) {
|
|
287
|
+
return typeof value === "object"
|
|
288
|
+
&& value !== null
|
|
289
|
+
&& value[LIVE_INPUT_INSTRUMENTED] === true;
|
|
94
290
|
}
|
package/src/ai/runtime/router.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
// Result:
|
|
29
29
|
// The success run's result, with `failoverHistory` appended describing every
|
|
30
30
|
// prior attempt: [{ model, failureKind, requestId, retryableSubkind }].
|
|
31
|
+
// Successful retries/backups withhold providerSessionId: their stateless
|
|
32
|
+
// answer cannot synchronize the primary provider session.
|
|
31
33
|
// If every eligible retryable/auth entry in the chain fails, returns the last
|
|
32
34
|
// result with `failureKind: "provider_unavailable_exhausted"`. Terminal
|
|
33
35
|
// non-retryable failures are returned as-is with their failover history.
|
|
@@ -42,6 +44,7 @@ import { passthroughSandbox } from "../../agent/sandbox-seam.js";
|
|
|
42
44
|
import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
43
45
|
import { createObserverHub } from "../observer.js";
|
|
44
46
|
import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
47
|
+
import { createWebSearchRunState } from "../../agent/tools/web-search-state.js";
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
|
|
@@ -89,14 +92,17 @@ import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
|
89
92
|
|
|
90
93
|
const ATTEMPT_SCOPED_OPTION_KEYS = ["customProvider", "customModel", "modelCapabilities", "isPrivateProvider"];
|
|
91
94
|
const ROUTER_TOOL_CONTEXT_KEYS = [
|
|
92
|
-
"workspace", "repoRoot", "
|
|
95
|
+
"workspace", "repoRoot", "additionalReadRoots", "additionalWriteRoots",
|
|
96
|
+
"ripgrepPath", "qaOutputDir", "sandboxPolicy", "sandboxEngine",
|
|
93
97
|
];
|
|
94
98
|
const RESOLVER_PROTECTED_OPTION_KEYS = new Set([
|
|
95
99
|
"model", "effort", "messages", "abortSignal", "onEvent",
|
|
96
|
-
"sessionId", "providerSessionId", "sessionKeepAlive", "sessionIdleTimeoutMs",
|
|
100
|
+
"sessionRecovery", "sessionId", "providerSessionId", "providerAttributionSessionId", "sessionKeepAlive", "sessionIdleTimeoutMs",
|
|
97
101
|
"diagnosticsSeed", "systemPromptPrefix", "sandboxPolicy", "sandboxEngine", "sandbox",
|
|
98
102
|
"allowedTools", "disallowedTools", "permissionMode", "mcpServers", "mcpApps", "skills",
|
|
99
|
-
"
|
|
103
|
+
"mcpCallNoTotalTimeoutTools",
|
|
104
|
+
"webSearchState",
|
|
105
|
+
"outputSchema", "liveInput", "toolEnvironment", "persistArtifact",
|
|
100
106
|
]);
|
|
101
107
|
|
|
102
108
|
class ResolverProtectedOptionError extends Error {
|
|
@@ -141,6 +147,10 @@ export function createRouterRuntime({ host = {}, chain = [], resolveAttempt, ret
|
|
|
141
147
|
* @returns {Promise<RuntimeResult>}
|
|
142
148
|
*/
|
|
143
149
|
async run(systemPrompt, options = {}) {
|
|
150
|
+
options = {
|
|
151
|
+
...options,
|
|
152
|
+
webSearchState: createWebSearchRunState(options.webSearchConfig, options.webSearchState),
|
|
153
|
+
};
|
|
144
154
|
const liveInputHub = options.liveInput === undefined
|
|
145
155
|
? undefined
|
|
146
156
|
: createObserverHub({
|
|
@@ -247,13 +257,12 @@ export function createRouterRuntime({ host = {}, chain = [], resolveAttempt, ret
|
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
applyEntryEffort(callOptions, entry.effort);
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (entries.length > 1 || i > 0 || retryIndex > 0 || !entrySupportsSessionResume(entry)) {
|
|
260
|
+
// Only the primary's first attempt may own a provider session. Retries
|
|
261
|
+
// replay the logical turn and must not resume a transcript the failed
|
|
262
|
+
// attempt may have appended to; backup routes never inherit that session.
|
|
263
|
+
const sessionEligibleAttempt = i === 0 && retryIndex === 0 && entrySupportsSessionResume(entry);
|
|
264
|
+
if (!sessionEligibleAttempt) {
|
|
265
|
+
delete callOptions.sessionRecovery;
|
|
257
266
|
delete callOptions.sessionId;
|
|
258
267
|
delete callOptions.providerSessionId;
|
|
259
268
|
delete callOptions.sessionKeepAlive;
|
|
@@ -311,6 +320,10 @@ export function createRouterRuntime({ host = {}, chain = [], resolveAttempt, ret
|
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
result = normalizeProviderAuthFailure(result);
|
|
323
|
+
if (!sessionEligibleAttempt) {
|
|
324
|
+
const { providerSessionRecovery: _receipt, ...unownedResult } = result;
|
|
325
|
+
result = unownedResult;
|
|
326
|
+
}
|
|
314
327
|
|
|
315
328
|
const retryability = retryableProviderFailureInfo({
|
|
316
329
|
errorText: result.error || "",
|
|
@@ -333,7 +346,10 @@ export function createRouterRuntime({ host = {}, chain = [], resolveAttempt, ret
|
|
|
333
346
|
model: modelKey(entry.model),
|
|
334
347
|
});
|
|
335
348
|
}
|
|
336
|
-
|
|
349
|
+
// Pi may report the attribution id as its session id even on a
|
|
350
|
+
// stateless call. Withhold that resumable id so host history cannot
|
|
351
|
+
// synchronize an untouched or failed primary transcript.
|
|
352
|
+
return { ...result, ...(sessionEligibleAttempt ? {} : { providerSessionId: undefined }), failoverHistory };
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
failoverHistory.push({
|
|
@@ -428,6 +444,9 @@ export function createRouterRuntime({ host = {}, chain = [], resolveAttempt, ret
|
|
|
428
444
|
configuredTools = { ...(configuredTools || {}), ...next };
|
|
429
445
|
inner.configureTools?.(next);
|
|
430
446
|
},
|
|
447
|
+
async recoverSession(receipt, context) {
|
|
448
|
+
return await inner.recoverSession?.(receipt, context) === true;
|
|
449
|
+
},
|
|
431
450
|
async syncSession(providerSessionId) {
|
|
432
451
|
return Boolean(await inner.syncSession?.(providerSessionId));
|
|
433
452
|
},
|
package/src/ai/tool-lifecycle.js
CHANGED
|
@@ -15,9 +15,9 @@ const HOST_HISTORY_METADATA = Symbol("mono-agent.host-tool-history");
|
|
|
15
15
|
const HOST_TOOL_LIFECYCLE_METADATA = Symbol("mono-agent.host-tool-lifecycle");
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* @param {{sink?: (event: any) => Promise<any>, onObserve?: (event: any) => void, onEvent?: (event: any) => void, abortSignal?: AbortSignal}} options
|
|
18
|
+
* @param {{sink?: (event: any) => Promise<any>, onObserve?: (event: any) => void, onLifecycleAdmitted?: (event: any) => void, onEvent?: (event: any) => void, abortSignal?: AbortSignal}} options
|
|
19
19
|
*/
|
|
20
|
-
export function createToolLifecycleEventGate({ sink, onObserve, onEvent, abortSignal }) {
|
|
20
|
+
export function createToolLifecycleEventGate({ sink, onObserve, onLifecycleAdmitted, onEvent, abortSignal }) {
|
|
21
21
|
/** @type {Promise<void>} */
|
|
22
22
|
let tail = Promise.resolve();
|
|
23
23
|
let pendingDeliveries = 0;
|
|
@@ -25,22 +25,30 @@ export function createToolLifecycleEventGate({ sink, onObserve, onEvent, abortSi
|
|
|
25
25
|
const timing = new Map();
|
|
26
26
|
/** @type {Map<string, any>} */
|
|
27
27
|
const approvals = new Map();
|
|
28
|
+
const preparedBlocks = new WeakSet();
|
|
28
29
|
|
|
29
30
|
const emit = (event) => {
|
|
30
31
|
stripProviderLifecycleMetadata(event);
|
|
31
32
|
try { onObserve?.(event); } catch { /* observer callback semantics remain best-effort */ }
|
|
33
|
+
observeClassification(event, timing, approvals);
|
|
32
34
|
const requiresPersistence = typeof sink === "function" && eventNeedsPersistence(event);
|
|
35
|
+
// Admission/classification follows native emission, not delayed storage.
|
|
36
|
+
const writes = requiresPersistence ? prepareEvent(event, { timing, approvals, abortSignal }, preparedBlocks) : [];
|
|
37
|
+
for (const write of writes) {
|
|
38
|
+
try { onLifecycleAdmitted?.(write.lifecycle); } catch { /* observers remain best-effort */ }
|
|
39
|
+
}
|
|
33
40
|
if (!requiresPersistence && pendingDeliveries === 0) {
|
|
34
|
-
observeClassification(event, timing, approvals);
|
|
35
41
|
try { onEvent?.(event); } catch { /* host callback semantics remain best-effort */ }
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
pendingDeliveries += 1;
|
|
40
46
|
const delivery = tail.then(async () => {
|
|
41
|
-
observeClassification(event, timing, approvals);
|
|
42
47
|
if (requiresPersistence) {
|
|
43
|
-
|
|
48
|
+
for (const write of writes) {
|
|
49
|
+
const persisted = await safePersist(sink, write.lifecycle);
|
|
50
|
+
try { write.block.history = historyMetadata(persisted, write.state); } catch { /* still settle every admitted write */ }
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
try { onEvent?.(event); } catch { /* host callback semantics remain best-effort */ }
|
|
46
54
|
}).catch((error) => {
|
|
@@ -118,32 +126,34 @@ function observeClassification(event, timing, approvals) {
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
/** @param {any} event @param {
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
/** @param {any} event @param {{timing:Map<string,any>,approvals:Map<string,any>,abortSignal?:AbortSignal}} context @param {WeakSet<object>} preparedBlocks */
|
|
130
|
+
function prepareEvent(event, context, preparedBlocks) {
|
|
131
|
+
const writes = [];
|
|
132
|
+
if (!record(event) || (event.type !== "assistant" && event.type !== "user")) return writes;
|
|
124
133
|
const message = event.message;
|
|
125
|
-
if (!record(message) || !Array.isArray(message.content)) return;
|
|
134
|
+
if (!record(message) || !Array.isArray(message.content)) return writes;
|
|
126
135
|
for (const block of message.content) {
|
|
127
136
|
if (!record(block)) continue;
|
|
128
137
|
if (event.type === "assistant" && block.type === "tool_use") {
|
|
129
|
-
if (hostHistoryMetadata(block.history)) continue;
|
|
138
|
+
if (hostHistoryMetadata(block.history) || preparedBlocks.has(block)) continue;
|
|
130
139
|
if (typeof block.id !== "string" || typeof block.name !== "string") continue;
|
|
131
|
-
const
|
|
140
|
+
const lifecycle = {
|
|
132
141
|
phase: "invocation",
|
|
133
142
|
toolCallId: block.id,
|
|
134
143
|
toolName: block.name,
|
|
135
144
|
...(Object.hasOwn(block, "input") ? { arguments: block.input } : {}),
|
|
136
|
-
}
|
|
137
|
-
|
|
145
|
+
};
|
|
146
|
+
preparedBlocks.add(block);
|
|
147
|
+
writes.push({ block, lifecycle, state: undefined });
|
|
138
148
|
continue;
|
|
139
149
|
}
|
|
140
150
|
if (event.type === "user" && block.type === "tool_result") {
|
|
141
|
-
if (hostHistoryMetadata(block.history)) continue;
|
|
151
|
+
if (hostHistoryMetadata(block.history) || preparedBlocks.has(block)) continue;
|
|
142
152
|
const id = typeof block.tool_use_id === "string" ? block.tool_use_id
|
|
143
153
|
: typeof block.tool_call_id === "string" ? block.tool_call_id : undefined;
|
|
144
154
|
if (id === undefined) continue;
|
|
145
155
|
const classified = classifyGenericResult(block, context.timing.get(id), context.approvals.get(id), context.abortSignal);
|
|
146
|
-
const
|
|
156
|
+
const lifecycle = {
|
|
147
157
|
phase: "result",
|
|
148
158
|
toolCallId: id,
|
|
149
159
|
...(typeof block.name === "string" ? { toolName: block.name } : {}),
|
|
@@ -153,12 +163,14 @@ async function persistEvent(event, sink, context) {
|
|
|
153
163
|
? { executionMs: context.timing.get(id).execution_ms }
|
|
154
164
|
: {}),
|
|
155
165
|
artifacts: artifactPaths(block),
|
|
156
|
-
}
|
|
157
|
-
|
|
166
|
+
};
|
|
167
|
+
preparedBlocks.add(block);
|
|
168
|
+
writes.push({ block, lifecycle, state: classified.state });
|
|
158
169
|
context.timing.delete(id);
|
|
159
170
|
context.approvals.delete(id);
|
|
160
171
|
}
|
|
161
172
|
}
|
|
173
|
+
return writes;
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
/** @param {any} block @param {any} timing @param {any} approval @param {AbortSignal|undefined} abortSignal */
|
|
@@ -219,7 +231,9 @@ export function historyMetadata(persisted, terminalStateValue) {
|
|
|
219
231
|
const metadata = {
|
|
220
232
|
...(typeof persisted?.recordId === "string" ? { recordId: persisted.recordId } : {}),
|
|
221
233
|
...(Number.isFinite(Number(persisted?.sequence)) ? { sequence: Number(persisted.sequence) } : {}),
|
|
222
|
-
persistence: persisted?.persistence === "persisted"
|
|
234
|
+
persistence: persisted?.persistence === "persisted" || persisted?.persistence === "deferred"
|
|
235
|
+
? persisted.persistence
|
|
236
|
+
: "failed",
|
|
223
237
|
...(terminalStateValue === undefined ? {} : { terminalState: terminalStateValue }),
|
|
224
238
|
...(typeof persisted?.truncated === "boolean" ? { truncated: persisted.truncated } : {}),
|
|
225
239
|
...(Number.isFinite(Number(persisted?.originalBytes)) ? { originalBytes: Number(persisted.originalBytes) } : {}),
|