@alisio/core 0.1.0-alpha.21 → 0.1.0-alpha.23
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/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +41 -0
- package/dist/application.js +197 -0
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/commands/builtins.js +13 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.js +186 -8
- package/dist/core/contracts.d.ts +69 -1
- package/dist/core/runner.d.ts +53 -4
- package/dist/core/runner.js +549 -161
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/index.d.ts +23 -4
- package/dist/index.js +22 -3
- package/dist/mcp/rich.js +4 -0
- package/dist/plugins/host.js +14 -6
- package/dist/runtime/blobs.d.ts +22 -0
- package/dist/runtime/blobs.js +84 -1
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +5 -0
- package/dist/runtime/paths.js +6 -0
- package/dist/runtime/process.d.ts +12 -0
- package/dist/runtime/process.js +64 -4
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +5 -0
- package/dist/runtime/store.js +75 -0
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +4 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +572 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.js +6 -2
- package/package.json +2 -2
package/dist/core/runner.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { isEphemeralRunEventType, textProjection, textResult, } from "@alisio/sdk";
|
|
2
2
|
import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessagesToBudget, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
|
|
3
|
+
import { isTimeoutReason, providerLabel, RunTimeoutError } from "./timeout.js";
|
|
4
|
+
/** Pause before a silent request is sent again; short, and cut off by a stop or the run limit. */
|
|
5
|
+
const FIRST_TOKEN_RETRY_PAUSE_MS = 250;
|
|
6
|
+
function abortableDelay(ms, signal) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
if (signal.aborted)
|
|
9
|
+
return resolve();
|
|
10
|
+
const done = () => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
signal.removeEventListener("abort", done);
|
|
13
|
+
resolve();
|
|
14
|
+
};
|
|
15
|
+
const timer = setTimeout(done, ms);
|
|
16
|
+
signal.addEventListener("abort", done, { once: true });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
3
19
|
/**
|
|
4
20
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
|
5
21
|
* context), clamped; 1M tokens when the window is unknown.
|
|
@@ -17,7 +33,19 @@ const MAX_INJECT_CHARS = 24_000;
|
|
|
17
33
|
* fails with an actionable error instead of silently losing the whole history.
|
|
18
34
|
*/
|
|
19
35
|
const MIN_MESSAGE_BUDGET_FLOOR = 4_000;
|
|
20
|
-
const allowed = (policy, effect) => effect === "read" ||
|
|
36
|
+
const allowed = (policy, effect) => effect === "read" ||
|
|
37
|
+
effect === "internal" ||
|
|
38
|
+
((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
|
|
39
|
+
/** A tool's capability is pre-allowed by the policy alone (`--allow-analysis`). */
|
|
40
|
+
const capabilityAllowed = (policy, tool) => tool.capability === "analysis.run" && !!policy.analysis;
|
|
41
|
+
/** First 40 lines of a script input, for capability approvals. */
|
|
42
|
+
const scriptPreview = (input) => typeof input.code === "string" ? input.code.split(/\r?\n/).slice(0, 40).join("\n") : undefined;
|
|
43
|
+
/** A short id for a tool call the user started from a UI (alphanumeric, 9 characters). */
|
|
44
|
+
const uiCallId = () => {
|
|
45
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
46
|
+
const bytes = crypto.getRandomValues(new Uint8Array(9));
|
|
47
|
+
return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
|
|
48
|
+
};
|
|
21
49
|
export class AgentRunner {
|
|
22
50
|
options;
|
|
23
51
|
/** Sessions with an active run or compaction (one at a time per session). */
|
|
@@ -80,8 +108,11 @@ export class AgentRunner {
|
|
|
80
108
|
}
|
|
81
109
|
};
|
|
82
110
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Tools offered to the model under the current policy (and optional run narrowing). With a
|
|
113
|
+
* session, a capability tool is also offered when that session's root holds a persisted grant.
|
|
114
|
+
*/
|
|
115
|
+
availableTools(run = {}, sessionId) {
|
|
85
116
|
const o = this.options;
|
|
86
117
|
const policy = run.policy ?? o.policy;
|
|
87
118
|
const approvals = !!o.approve && run.approvals !== false;
|
|
@@ -91,6 +122,12 @@ export class AgentRunner {
|
|
|
91
122
|
const effect = t.effect ?? "external";
|
|
92
123
|
if (allowed(policy, effect))
|
|
93
124
|
return true;
|
|
125
|
+
if (t.capability) {
|
|
126
|
+
if (capabilityAllowed(policy, t))
|
|
127
|
+
return true;
|
|
128
|
+
if (sessionId && o.capabilities?.granted(t.capability, sessionId))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
94
131
|
return approvals && (effect === "write" || effect === "process" || effect === "external");
|
|
95
132
|
});
|
|
96
133
|
}
|
|
@@ -310,6 +347,355 @@ export class AgentRunner {
|
|
|
310
347
|
throw error;
|
|
311
348
|
}
|
|
312
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Gates and executes one tool call: external-directory paths, the capability/effect gate with
|
|
352
|
+
* its approval, the call journal, artifact publishing and the result bound. Shared by model
|
|
353
|
+
* runs and `runToolCall`, so a call started from a UI passes exactly the same gates.
|
|
354
|
+
*/
|
|
355
|
+
async executeCall(scope, p) {
|
|
356
|
+
const o = this.options;
|
|
357
|
+
const { sessionId, runId, emit, workspace, policy, approvals, options } = scope;
|
|
358
|
+
const combined = scope.signal;
|
|
359
|
+
const pathAccess = o.pathAccess;
|
|
360
|
+
const { call } = p;
|
|
361
|
+
let result;
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
// `effect` rides along so the TUI can group consecutive calls by capability kind
|
|
364
|
+
// (read batches, write batches, commands…) without a name heuristic. Additive field.
|
|
365
|
+
emit("tool_started", {
|
|
366
|
+
name: call.name,
|
|
367
|
+
id: call.id,
|
|
368
|
+
arguments: call.arguments,
|
|
369
|
+
effect: p.tool?.effect ?? "external",
|
|
370
|
+
});
|
|
371
|
+
try {
|
|
372
|
+
combined.throwIfAborted();
|
|
373
|
+
if (p.error)
|
|
374
|
+
throw new Error(p.error);
|
|
375
|
+
if (!p.tool || !p.input)
|
|
376
|
+
throw new Error("Invalid tool");
|
|
377
|
+
const effect = p.tool.effect ?? "external";
|
|
378
|
+
// External-directory prerequisite: declared paths are resolved (and, outside every
|
|
379
|
+
// allowed root, approved for the containing directory) BEFORE the effect gate, so a
|
|
380
|
+
// write still needs its own write policy and a read never hard-throws first. The
|
|
381
|
+
// resolved map keeps "allow once" scoped to this one call, with no shared mutable
|
|
382
|
+
// state that parallel read batches could race on.
|
|
383
|
+
const resolved = new Map();
|
|
384
|
+
if (pathAccess)
|
|
385
|
+
for (const declared of p.tool.paths?.(p.input) ?? [])
|
|
386
|
+
if (!resolved.has(declared)) {
|
|
387
|
+
const target = await pathAccess.resolve(declared, {
|
|
388
|
+
workspace,
|
|
389
|
+
session: sessionId,
|
|
390
|
+
...(options.label ? { label: options.label } : {}),
|
|
391
|
+
signal: combined,
|
|
392
|
+
});
|
|
393
|
+
// Key both the declared input and its resolved form: a write resolves the
|
|
394
|
+
// input once, then re-checks the already-absolute path, and neither lookup may
|
|
395
|
+
// ask a second time for an "allow once" decision.
|
|
396
|
+
resolved.set(declared, target);
|
|
397
|
+
resolved.set(target, target);
|
|
398
|
+
}
|
|
399
|
+
scope.guard?.(effect);
|
|
400
|
+
if (options.toolFilter && !options.toolFilter(p.tool))
|
|
401
|
+
throw new Error(`Tool ${call.name} is not available in this session`);
|
|
402
|
+
const capability = p.tool.capability;
|
|
403
|
+
const gate = o.capabilities;
|
|
404
|
+
if (capability && (allowed(policy, effect) || capabilityAllowed(policy, p.tool))) {
|
|
405
|
+
// A broad process grant or --allow-analysis: allowed, audited as a flag decision.
|
|
406
|
+
gate?.record({
|
|
407
|
+
capability,
|
|
408
|
+
sessionId,
|
|
409
|
+
scope: "once",
|
|
410
|
+
decision: "allow",
|
|
411
|
+
source: "flag",
|
|
412
|
+
callId: call.id,
|
|
413
|
+
runId,
|
|
414
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else if (capability && gate?.granted(capability, sessionId)) {
|
|
418
|
+
// A persisted "allow for this session" grant of the root session.
|
|
419
|
+
}
|
|
420
|
+
else if (!allowed(policy, effect)) {
|
|
421
|
+
if (!o.approve ||
|
|
422
|
+
!approvals ||
|
|
423
|
+
(effect !== "write" && effect !== "process" && effect !== "external"))
|
|
424
|
+
throw new Error(`Capability denied: ${capability ?? effect}`);
|
|
425
|
+
const preview = capability
|
|
426
|
+
? (scriptPreview(p.input) ?? (await gate?.preview?.(p.input, sessionId)))
|
|
427
|
+
: undefined;
|
|
428
|
+
emit("approval_requested", {
|
|
429
|
+
id: call.id,
|
|
430
|
+
name: call.name,
|
|
431
|
+
effect,
|
|
432
|
+
...(options.label ? { label: options.label } : {}),
|
|
433
|
+
...(capability ? { capability } : {}),
|
|
434
|
+
});
|
|
435
|
+
const decision = await o.approve({
|
|
436
|
+
call,
|
|
437
|
+
effect,
|
|
438
|
+
input: p.input,
|
|
439
|
+
signal: combined,
|
|
440
|
+
session: sessionId,
|
|
441
|
+
...(options.label ? { label: options.label } : {}),
|
|
442
|
+
...(capability ? { capability } : {}),
|
|
443
|
+
...(preview !== undefined ? { preview } : {}),
|
|
444
|
+
...(capability && gate?.runtime ? { runtime: gate.runtime } : {}),
|
|
445
|
+
});
|
|
446
|
+
combined.throwIfAborted();
|
|
447
|
+
const persisted = !!capability && !!gate && decision === "session";
|
|
448
|
+
if (capability && gate)
|
|
449
|
+
gate.record({
|
|
450
|
+
capability,
|
|
451
|
+
sessionId,
|
|
452
|
+
scope: decision === "session" ? "session" : "once",
|
|
453
|
+
decision: decision === "deny" ? "deny" : "allow",
|
|
454
|
+
source: gate.source ?? "tui",
|
|
455
|
+
callId: call.id,
|
|
456
|
+
runId,
|
|
457
|
+
...(options.correlationId !== undefined
|
|
458
|
+
? { correlationId: options.correlationId }
|
|
459
|
+
: {}),
|
|
460
|
+
});
|
|
461
|
+
emit("approval_resolved", {
|
|
462
|
+
id: call.id,
|
|
463
|
+
name: call.name,
|
|
464
|
+
effect,
|
|
465
|
+
decision,
|
|
466
|
+
...(capability ? { capability } : {}),
|
|
467
|
+
...(persisted ? { persisted: true } : {}),
|
|
468
|
+
});
|
|
469
|
+
if (decision === "deny")
|
|
470
|
+
throw new Error(`Capability ${capability ?? effect} denied by the user for this call`);
|
|
471
|
+
// A capability grant never widens the effect: only effect approvals do.
|
|
472
|
+
if (decision === "session" && !capability)
|
|
473
|
+
policy[effect] = true;
|
|
474
|
+
}
|
|
475
|
+
o.store.beginCall(sessionId, call, { runId, effect });
|
|
476
|
+
const publisher = o.artifacts?.({ sessionId, runId, callId: call.id }, (published) => emit("artifact_published", {
|
|
477
|
+
artifact: published.artifact,
|
|
478
|
+
path: published.path,
|
|
479
|
+
callId: call.id,
|
|
480
|
+
...(published.executionId ? { executionId: published.executionId } : {}),
|
|
481
|
+
}));
|
|
482
|
+
/**
|
|
483
|
+
* `analysis.install`: always asks, `once` only, and no flag or earlier grant covers it. Where
|
|
484
|
+
* nobody can be asked (headless, no approval handler) the answer is `deny`.
|
|
485
|
+
*/
|
|
486
|
+
const approveInstall = async (install) => {
|
|
487
|
+
if (!o.approve || !approvals)
|
|
488
|
+
return "deny";
|
|
489
|
+
const installCall = { ...call, id: `${call.id}:install` };
|
|
490
|
+
emit("approval_requested", {
|
|
491
|
+
id: installCall.id,
|
|
492
|
+
name: call.name,
|
|
493
|
+
effect: "process",
|
|
494
|
+
...(options.label ? { label: options.label } : {}),
|
|
495
|
+
capability: "analysis.install",
|
|
496
|
+
install,
|
|
497
|
+
});
|
|
498
|
+
let decision = "deny";
|
|
499
|
+
try {
|
|
500
|
+
decision = await o.approve({
|
|
501
|
+
call: installCall,
|
|
502
|
+
effect: "process",
|
|
503
|
+
input: { extras: install.extras },
|
|
504
|
+
signal: combined,
|
|
505
|
+
session: sessionId,
|
|
506
|
+
...(options.label ? { label: options.label } : {}),
|
|
507
|
+
capability: "analysis.install",
|
|
508
|
+
install,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
combined.throwIfAborted();
|
|
513
|
+
}
|
|
514
|
+
// A `session` answer is treated as `once`: an installation permission is never stored.
|
|
515
|
+
const allowedNow = decision !== "deny";
|
|
516
|
+
gate?.record({
|
|
517
|
+
capability: "analysis.install",
|
|
518
|
+
sessionId,
|
|
519
|
+
scope: "once",
|
|
520
|
+
decision: allowedNow ? "allow" : "deny",
|
|
521
|
+
source: gate.source ?? "tui",
|
|
522
|
+
callId: call.id,
|
|
523
|
+
runId,
|
|
524
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
525
|
+
});
|
|
526
|
+
emit("approval_resolved", {
|
|
527
|
+
id: installCall.id,
|
|
528
|
+
name: call.name,
|
|
529
|
+
effect: "process",
|
|
530
|
+
decision: allowedNow ? "once" : "deny",
|
|
531
|
+
capability: "analysis.install",
|
|
532
|
+
});
|
|
533
|
+
return allowedNow ? "once" : "deny";
|
|
534
|
+
};
|
|
535
|
+
result = await p.tool.execute(p.input, {
|
|
536
|
+
signal: combined,
|
|
537
|
+
workspace,
|
|
538
|
+
session: sessionId,
|
|
539
|
+
runId,
|
|
540
|
+
callId: call.id,
|
|
541
|
+
...(publisher ? { artifacts: publisher } : {}),
|
|
542
|
+
approveInstall,
|
|
543
|
+
emit: (data) => emit("tool_progress", { id: call.id, data }),
|
|
544
|
+
...(options.label ? { label: options.label } : {}),
|
|
545
|
+
...(pathAccess
|
|
546
|
+
? {
|
|
547
|
+
resolvePath: (path) => {
|
|
548
|
+
const known = resolved.get(path);
|
|
549
|
+
return known !== undefined
|
|
550
|
+
? Promise.resolve(known)
|
|
551
|
+
: pathAccess.resolve(path, {
|
|
552
|
+
workspace,
|
|
553
|
+
session: sessionId,
|
|
554
|
+
...(options.label ? { label: options.label } : {}),
|
|
555
|
+
signal: combined,
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
: {}),
|
|
560
|
+
});
|
|
561
|
+
// The bound applies to what the model sees (the text projection): display-only
|
|
562
|
+
// ui/image parts are bounded by their producers and must not push text over it.
|
|
563
|
+
const projected = textProjection(result);
|
|
564
|
+
if (JSON.stringify(projected).length > 48_000)
|
|
565
|
+
result = textResult(`${JSON.stringify(projected).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
|
|
566
|
+
result = scope.amend?.(result, effect) ?? result;
|
|
567
|
+
o.store.endCall(sessionId, call, result);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
result = textResult(error instanceof Error ? error.message : String(error), true);
|
|
571
|
+
// A cancellation after execution may have left effects; journal remains pending.
|
|
572
|
+
if (!combined.aborted)
|
|
573
|
+
o.store.endCall(sessionId, call, result);
|
|
574
|
+
}
|
|
575
|
+
emit("tool_completed", {
|
|
576
|
+
id: call.id,
|
|
577
|
+
name: call.name,
|
|
578
|
+
isError: result.isError ?? false,
|
|
579
|
+
durationMs: Date.now() - started,
|
|
580
|
+
preview: result.content
|
|
581
|
+
.filter((part) => part.type === "text")
|
|
582
|
+
.map((c) => c.text)
|
|
583
|
+
.join("\n")
|
|
584
|
+
.slice(0, 2_000),
|
|
585
|
+
});
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Runs ONE tool call without a model (spec §20 `runToolCall`): an action the user started from
|
|
590
|
+
* a UI, such as copying an artifact into the workspace. It is a real run of the session
|
|
591
|
+
* (`run_started` → `tool_*` → `run_completed`, journaled) and the call passes exactly the gates
|
|
592
|
+
* of a model call (`write` approval, capabilities, paths). The transcript stays valid for every
|
|
593
|
+
* provider: a user note (shown as `display`), an assistant message with the call, the tool
|
|
594
|
+
* result and a short assistant summary.
|
|
595
|
+
*/
|
|
596
|
+
async runToolCall(sessionId, name, input, options = {}) {
|
|
597
|
+
const controller = this.claim(sessionId);
|
|
598
|
+
const runId = options.runId ?? crypto.randomUUID();
|
|
599
|
+
const o = this.options, emit = this.emitter(sessionId, { runId, correlationId: options.correlationId });
|
|
600
|
+
const finish = (status, error) => {
|
|
601
|
+
try {
|
|
602
|
+
o.store.endRun?.(runId, {
|
|
603
|
+
status,
|
|
604
|
+
usage: { input: 0, output: 0 },
|
|
605
|
+
...(error ? { error } : {}),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
/* A journal write failure cannot invalidate an executed run. */
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
|
|
613
|
+
const timeout = AbortSignal.timeout(runTimeoutMs);
|
|
614
|
+
const combined = AbortSignal.any([
|
|
615
|
+
controller.signal,
|
|
616
|
+
timeout,
|
|
617
|
+
...(options.signal ? [options.signal] : []),
|
|
618
|
+
]);
|
|
619
|
+
const workspace = options.workspace ?? o.workspace;
|
|
620
|
+
let acquired = false;
|
|
621
|
+
try {
|
|
622
|
+
o.store.beginRun?.({
|
|
623
|
+
id: runId,
|
|
624
|
+
session: sessionId,
|
|
625
|
+
status: "running",
|
|
626
|
+
model: o.store.get(sessionId).model,
|
|
627
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
628
|
+
});
|
|
629
|
+
o.store.acquire(sessionId);
|
|
630
|
+
acquired = true;
|
|
631
|
+
o.store.reconcile(sessionId);
|
|
632
|
+
const session = o.store.get(sessionId);
|
|
633
|
+
if (session.workspace !== workspace)
|
|
634
|
+
throw new Error("Session workspace mismatch");
|
|
635
|
+
combined.throwIfAborted();
|
|
636
|
+
const call = { id: uiCallId(), name, arguments: JSON.stringify(input) };
|
|
637
|
+
let prepared;
|
|
638
|
+
try {
|
|
639
|
+
prepared = {
|
|
640
|
+
call,
|
|
641
|
+
tool: o.registry.get(name),
|
|
642
|
+
input: o.registry.parse(name, call.arguments),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
prepared = { call, error: String(error) };
|
|
647
|
+
}
|
|
648
|
+
const note = `The user ran the ${name} tool from the interface.`;
|
|
649
|
+
o.store.append(sessionId, { role: "user", text: note, display: options.display ?? note });
|
|
650
|
+
emit("run_started", { model: session.model });
|
|
651
|
+
o.store.append(sessionId, { role: "assistant", text: "", calls: [call] });
|
|
652
|
+
const result = await this.executeCall({
|
|
653
|
+
sessionId,
|
|
654
|
+
runId,
|
|
655
|
+
emit,
|
|
656
|
+
signal: combined,
|
|
657
|
+
workspace,
|
|
658
|
+
policy: options.policy ?? o.policy,
|
|
659
|
+
approvals: !!o.approve && options.approvals !== false,
|
|
660
|
+
options,
|
|
661
|
+
}, prepared);
|
|
662
|
+
o.store.append(sessionId, { role: "tool", callId: call.id, result });
|
|
663
|
+
const first = textProjection(result)
|
|
664
|
+
.content.map((part) => (part.type === "text" ? part.text : ""))
|
|
665
|
+
.join(" ")
|
|
666
|
+
.replace(/\s*\n\s*/g, " ")
|
|
667
|
+
.trim()
|
|
668
|
+
.slice(0, 300);
|
|
669
|
+
const text = `${name} ${result.isError ? "failed" : "finished"}${first ? `: ${first}` : ""}`;
|
|
670
|
+
o.store.append(sessionId, { role: "assistant", text, calls: [] });
|
|
671
|
+
emit("run_completed", { tokens: 0, text });
|
|
672
|
+
finish("completed");
|
|
673
|
+
return { runId, status: "completed", result };
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (combined.aborted && isTimeoutReason(combined.reason)) {
|
|
677
|
+
const failure = new RunTimeoutError({
|
|
678
|
+
kind: "run",
|
|
679
|
+
ms: runTimeoutMs,
|
|
680
|
+
stage: "tool",
|
|
681
|
+
tool: name,
|
|
682
|
+
});
|
|
683
|
+
emit("run_failed", { error: failure.message, code: "timeout", timeout: failure.info });
|
|
684
|
+
finish("failed", failure.message);
|
|
685
|
+
throw failure;
|
|
686
|
+
}
|
|
687
|
+
const cause = combined.aborted ? (combined.reason ?? error) : error;
|
|
688
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
689
|
+
emit(combined.aborted ? "run_cancelled" : "run_failed", { error: message });
|
|
690
|
+
finish(combined.aborted ? "cancelled" : "failed", message);
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
if (acquired)
|
|
695
|
+
o.store.release(sessionId);
|
|
696
|
+
this.active.delete(sessionId);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
313
699
|
async run(sessionId, prompt, signal, options = {}) {
|
|
314
700
|
const controller = this.claim(sessionId);
|
|
315
701
|
const runId = options.runId ?? crypto.randomUUID();
|
|
@@ -326,8 +712,21 @@ export class AgentRunner {
|
|
|
326
712
|
const limit = o.maxContextChars ?? 800_000;
|
|
327
713
|
let tokens = 0, lastText = "", acquired = false;
|
|
328
714
|
const usageTotal = { input: 0, output: 0 };
|
|
329
|
-
const
|
|
715
|
+
const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
|
|
716
|
+
const firstTokenMs = options.firstTokenTimeoutMs ?? o.firstTokenTimeoutMs ?? 0;
|
|
717
|
+
const firstTokenRetries = firstTokenMs > 0 ? Math.max(0, options.firstTokenRetries ?? o.firstTokenRetries ?? 0) : 0;
|
|
718
|
+
const runStartedAt = Date.now();
|
|
719
|
+
const timeout = AbortSignal.timeout(runTimeoutMs);
|
|
330
720
|
const combined = AbortSignal.any([controller.signal, timeout, ...(signal ? [signal] : [])]);
|
|
721
|
+
// What the run was doing, so a timeout can say where it was stuck (see `RunTimeoutError`).
|
|
722
|
+
let stage = "other";
|
|
723
|
+
let stageTool;
|
|
724
|
+
let modelId;
|
|
725
|
+
let providerName;
|
|
726
|
+
let requests = 0;
|
|
727
|
+
let silentTimedOut = false;
|
|
728
|
+
/** Sends of the request that is (or was last) waiting for its first token. */
|
|
729
|
+
let silentAttempts = 0;
|
|
331
730
|
const context = options.context ?? o.context;
|
|
332
731
|
const workspace = options.workspace ?? o.workspace;
|
|
333
732
|
const pathAccess = o.pathAccess;
|
|
@@ -354,6 +753,8 @@ export class AgentRunner {
|
|
|
354
753
|
throw new Error("Session provider/workspace mismatch");
|
|
355
754
|
// The session records the model; switching models is explicit via setModel.
|
|
356
755
|
const model = session.model;
|
|
756
|
+
modelId = model;
|
|
757
|
+
providerName = providerLabel(provider.id);
|
|
357
758
|
combined.throwIfAborted();
|
|
358
759
|
if (o.extensions && o.store.messages(sessionId).length === 0) {
|
|
359
760
|
// New session: extensions may inject budgeted context once, persisted with the session.
|
|
@@ -394,6 +795,7 @@ export class AgentRunner {
|
|
|
394
795
|
text: prompt,
|
|
395
796
|
...(options.display ? { display: options.display } : {}),
|
|
396
797
|
...(options.attachments?.length ? { attachments: options.attachments } : {}),
|
|
798
|
+
...(options.datasets?.length ? { datasets: options.datasets } : {}),
|
|
397
799
|
});
|
|
398
800
|
emit("run_started", { model });
|
|
399
801
|
const budget = options.maxTokens ?? o.maxTokens ?? defaultTokenBudget(o.contextWindow?.(model));
|
|
@@ -407,7 +809,7 @@ export class AgentRunner {
|
|
|
407
809
|
o.store.append(sessionId, { role: "user", text: queued });
|
|
408
810
|
let instructions = await withPersona();
|
|
409
811
|
let messages = o.store.messages(sessionId);
|
|
410
|
-
const tools = this.availableTools(options);
|
|
812
|
+
const tools = this.availableTools(options, sessionId);
|
|
411
813
|
const toolsText = this.toolsText(tools);
|
|
412
814
|
const chars = () => instructions.length + JSON.stringify(messages).length + toolsText.length;
|
|
413
815
|
// REDUCIBLE content only (instructions + transcript). The fixed tool catalog
|
|
@@ -463,39 +865,104 @@ export class AgentRunner {
|
|
|
463
865
|
// display-only extension of the persisted transcript and never reach a model prompt
|
|
464
866
|
// (raw image bytes included). The store keeps the rich parts for TUI replay.
|
|
465
867
|
const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
|
|
466
|
-
|
|
868
|
+
let requested = Date.now();
|
|
467
869
|
let firstDelta;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
870
|
+
stage = "waiting_model";
|
|
871
|
+
requests++;
|
|
872
|
+
silentAttempts = 0;
|
|
873
|
+
// The same request is sent again only while the provider stayed completely silent until
|
|
874
|
+
// `firstTokenTimeoutMs`. Nothing was received, appended or accounted for the aborted
|
|
875
|
+
// attempt, so the retry reuses the exact same inputs inside this turn.
|
|
876
|
+
for (;;) {
|
|
877
|
+
silentAttempts++;
|
|
878
|
+
silentTimedOut = false;
|
|
879
|
+
requested = Date.now();
|
|
880
|
+
// A request that stays completely silent can be stopped early (`firstTokenTimeoutMs`).
|
|
881
|
+
const silent = new AbortController();
|
|
882
|
+
let silentTimer = firstTokenMs > 0
|
|
883
|
+
? setTimeout(() => {
|
|
884
|
+
silentTimedOut = true;
|
|
885
|
+
silent.abort();
|
|
886
|
+
}, firstTokenMs)
|
|
887
|
+
: undefined;
|
|
888
|
+
const stopSilentTimer = () => {
|
|
889
|
+
if (silentTimer)
|
|
890
|
+
clearTimeout(silentTimer);
|
|
891
|
+
silentTimer = undefined;
|
|
892
|
+
};
|
|
893
|
+
let failure;
|
|
894
|
+
try {
|
|
895
|
+
for await (const e of provider.stream({
|
|
896
|
+
instructions,
|
|
897
|
+
messages: providerMessages,
|
|
898
|
+
tools,
|
|
899
|
+
maxOutputTokens: options.maxOutputTokens ?? o.maxOutputTokens ?? 4096,
|
|
900
|
+
signal: firstTokenMs > 0 ? AbortSignal.any([combined, silent.signal]) : combined,
|
|
901
|
+
model,
|
|
902
|
+
sessionId,
|
|
903
|
+
...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
|
|
904
|
+
...((options.reasoningEffort ?? o.reasoningEffort)
|
|
905
|
+
? { reasoningEffort: options.reasoningEffort ?? o.reasoningEffort }
|
|
906
|
+
: {}),
|
|
907
|
+
})) {
|
|
908
|
+
combined.throwIfAborted();
|
|
909
|
+
stopSilentTimer();
|
|
910
|
+
if (stage === "waiting_model")
|
|
911
|
+
stage = "streaming";
|
|
912
|
+
if (e.type !== "completed")
|
|
913
|
+
firstDelta ??= Date.now();
|
|
914
|
+
if (e.type === "text_delta")
|
|
915
|
+
emit("text_delta", { delta: e.delta });
|
|
916
|
+
else if (e.type === "reasoning_delta")
|
|
917
|
+
emit("reasoning_delta", { delta: e.delta });
|
|
918
|
+
else {
|
|
919
|
+
if (completion)
|
|
920
|
+
throw new Error("Provider emitted multiple completions");
|
|
921
|
+
completion = e.message;
|
|
922
|
+
truncated = e.message.truncated === true;
|
|
923
|
+
usage = e.usage;
|
|
924
|
+
tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
|
|
925
|
+
usageTotal.input += e.usage?.input ?? 0;
|
|
926
|
+
usageTotal.output += e.usage?.output ?? 0;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
failure = error;
|
|
497
932
|
}
|
|
933
|
+
finally {
|
|
934
|
+
stopSilentTimer();
|
|
935
|
+
}
|
|
936
|
+
const silentFailure = silentTimedOut &&
|
|
937
|
+
firstDelta === undefined &&
|
|
938
|
+
!completion &&
|
|
939
|
+
!combined.aborted &&
|
|
940
|
+
silentAttempts <= firstTokenRetries;
|
|
941
|
+
if (!silentFailure) {
|
|
942
|
+
if (failure !== undefined)
|
|
943
|
+
throw failure;
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
// Do not start an attempt the whole-run limit could not let finish.
|
|
947
|
+
const retryPauseMs = FIRST_TOKEN_RETRY_PAUSE_MS;
|
|
948
|
+
if (runTimeoutMs - (Date.now() - runStartedAt) < retryPauseMs + firstTokenMs) {
|
|
949
|
+
if (failure !== undefined)
|
|
950
|
+
throw failure;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
// From here the silence is handled: a later stop or run timeout is judged on its own.
|
|
954
|
+
silentTimedOut = false;
|
|
955
|
+
emit("request_retry", {
|
|
956
|
+
attempt: silentAttempts,
|
|
957
|
+
of: firstTokenRetries,
|
|
958
|
+
reason: "first_token_timeout",
|
|
959
|
+
afterMs: Date.now() - requested,
|
|
960
|
+
});
|
|
961
|
+
await abortableDelay(retryPauseMs, combined);
|
|
962
|
+
combined.throwIfAborted();
|
|
963
|
+
stage = "waiting_model";
|
|
498
964
|
}
|
|
965
|
+
stage = "other";
|
|
499
966
|
if (!completion)
|
|
500
967
|
throw new Error("Provider stream ended without a completed response");
|
|
501
968
|
const durationMs = Date.now() - requested;
|
|
@@ -556,137 +1023,38 @@ export class AgentRunner {
|
|
|
556
1023
|
if (paths.length)
|
|
557
1024
|
contextUpdate = await context.beforePaths(paths, sessionId);
|
|
558
1025
|
let pendingUpdate = contextUpdate;
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
effect: p.tool?.effect ?? "external",
|
|
570
|
-
});
|
|
571
|
-
try {
|
|
572
|
-
combined.throwIfAborted();
|
|
573
|
-
if (p.error)
|
|
574
|
-
throw new Error(p.error);
|
|
575
|
-
if (!p.tool || !p.input)
|
|
576
|
-
throw new Error("Invalid tool");
|
|
577
|
-
const effect = p.tool.effect ?? "external";
|
|
578
|
-
// External-directory prerequisite: declared paths are resolved (and, outside every
|
|
579
|
-
// allowed root, approved for the containing directory) BEFORE the effect gate, so a
|
|
580
|
-
// write still needs its own write policy and a read never hard-throws first. The
|
|
581
|
-
// resolved map keeps "allow once" scoped to this one call, with no shared mutable
|
|
582
|
-
// state that parallel read batches could race on.
|
|
583
|
-
const resolved = new Map();
|
|
584
|
-
if (pathAccess)
|
|
585
|
-
for (const declared of p.tool.paths?.(p.input) ?? [])
|
|
586
|
-
if (!resolved.has(declared)) {
|
|
587
|
-
const target = await pathAccess.resolve(declared, {
|
|
588
|
-
workspace,
|
|
589
|
-
session: sessionId,
|
|
590
|
-
...(options.label ? { label: options.label } : {}),
|
|
591
|
-
signal: combined,
|
|
592
|
-
});
|
|
593
|
-
// Key both the declared input and its resolved form: a write resolves the
|
|
594
|
-
// input once, then re-checks the already-absolute path, and neither lookup may
|
|
595
|
-
// ask a second time for an "allow once" decision.
|
|
596
|
-
resolved.set(declared, target);
|
|
597
|
-
resolved.set(target, target);
|
|
598
|
-
}
|
|
1026
|
+
const scope = {
|
|
1027
|
+
sessionId,
|
|
1028
|
+
runId,
|
|
1029
|
+
emit,
|
|
1030
|
+
signal: combined,
|
|
1031
|
+
workspace,
|
|
1032
|
+
policy,
|
|
1033
|
+
approvals,
|
|
1034
|
+
options,
|
|
1035
|
+
guard: (effect) => {
|
|
599
1036
|
if (contextUpdate && effect !== "read")
|
|
600
1037
|
throw new Error(`Context changed; reconsider this call before retrying.\n${contextUpdate}`);
|
|
601
1038
|
if (tokens >= budget)
|
|
602
1039
|
throw new Error("Token budget exhausted; tool was not executed");
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (!
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
call,
|
|
618
|
-
effect,
|
|
619
|
-
input: p.input,
|
|
620
|
-
signal: combined,
|
|
621
|
-
session: sessionId,
|
|
622
|
-
...(options.label ? { label: options.label } : {}),
|
|
623
|
-
});
|
|
624
|
-
combined.throwIfAborted();
|
|
625
|
-
emit("approval_resolved", { id: call.id, name: call.name, effect, decision });
|
|
626
|
-
if (decision === "deny")
|
|
627
|
-
throw new Error(`Capability ${effect} denied by the user for this call`);
|
|
628
|
-
if (decision === "session")
|
|
629
|
-
policy[effect] = true;
|
|
630
|
-
}
|
|
631
|
-
o.store.beginCall(sessionId, call, { runId, effect });
|
|
632
|
-
result = await p.tool.execute(p.input, {
|
|
633
|
-
signal: combined,
|
|
634
|
-
workspace,
|
|
635
|
-
session: sessionId,
|
|
636
|
-
emit: (data) => emit("tool_progress", { id: call.id, data }),
|
|
637
|
-
...(options.label ? { label: options.label } : {}),
|
|
638
|
-
...(pathAccess
|
|
639
|
-
? {
|
|
640
|
-
resolvePath: (path) => {
|
|
641
|
-
const known = resolved.get(path);
|
|
642
|
-
return known !== undefined
|
|
643
|
-
? Promise.resolve(known)
|
|
644
|
-
: pathAccess.resolve(path, {
|
|
645
|
-
workspace,
|
|
646
|
-
session: sessionId,
|
|
647
|
-
...(options.label ? { label: options.label } : {}),
|
|
648
|
-
signal: combined,
|
|
649
|
-
});
|
|
650
|
-
},
|
|
651
|
-
}
|
|
652
|
-
: {}),
|
|
653
|
-
});
|
|
654
|
-
// The bound applies to what the model sees (the text projection): display-only
|
|
655
|
-
// ui/image parts are bounded by their producers and must not push text over it.
|
|
656
|
-
const projected = textProjection(result);
|
|
657
|
-
if (JSON.stringify(projected).length > 48_000)
|
|
658
|
-
result = textResult(`${JSON.stringify(projected).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
|
|
659
|
-
if (pendingUpdate && effect === "read") {
|
|
660
|
-
result = {
|
|
661
|
-
...result,
|
|
662
|
-
content: [
|
|
663
|
-
...result.content,
|
|
664
|
-
{ type: "text", text: `\n<instructions>\n${pendingUpdate}\n</instructions>` },
|
|
665
|
-
],
|
|
666
|
-
};
|
|
667
|
-
pendingUpdate = undefined;
|
|
668
|
-
}
|
|
669
|
-
o.store.endCall(sessionId, call, result);
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
result = textResult(error instanceof Error ? error.message : String(error), true);
|
|
673
|
-
// A cancellation after execution may have left effects; journal remains pending.
|
|
674
|
-
if (!combined.aborted)
|
|
675
|
-
o.store.endCall(sessionId, call, result);
|
|
676
|
-
}
|
|
677
|
-
emit("tool_completed", {
|
|
678
|
-
id: call.id,
|
|
679
|
-
name: call.name,
|
|
680
|
-
isError: result.isError ?? false,
|
|
681
|
-
durationMs: Date.now() - started,
|
|
682
|
-
preview: result.content
|
|
683
|
-
.filter((part) => part.type === "text")
|
|
684
|
-
.map((c) => c.text)
|
|
685
|
-
.join("\n")
|
|
686
|
-
.slice(0, 2_000),
|
|
687
|
-
});
|
|
688
|
-
return result;
|
|
1040
|
+
},
|
|
1041
|
+
amend: (result, effect) => {
|
|
1042
|
+
if (!pendingUpdate || effect !== "read")
|
|
1043
|
+
return result;
|
|
1044
|
+
const update = pendingUpdate;
|
|
1045
|
+
pendingUpdate = undefined;
|
|
1046
|
+
return {
|
|
1047
|
+
...result,
|
|
1048
|
+
content: [
|
|
1049
|
+
...result.content,
|
|
1050
|
+
{ type: "text", text: `\n<instructions>\n${update}\n</instructions>` },
|
|
1051
|
+
],
|
|
1052
|
+
};
|
|
1053
|
+
},
|
|
689
1054
|
};
|
|
1055
|
+
stage = "tool";
|
|
1056
|
+
stageTool = prepared[0]?.call.name;
|
|
1057
|
+
const execute = (p) => this.executeCall(scope, p);
|
|
690
1058
|
const results = [];
|
|
691
1059
|
// Bounded parallel batches for consecutive read or explicitly concurrent tools.
|
|
692
1060
|
const parallel = (p) => !!p?.tool && (p.tool.effect === "read" || p.tool.concurrent === true);
|
|
@@ -716,6 +1084,8 @@ export class AgentRunner {
|
|
|
716
1084
|
if (c && r)
|
|
717
1085
|
o.store.append(sessionId, { role: "tool", callId: c.id, result: r });
|
|
718
1086
|
}
|
|
1087
|
+
stage = "other";
|
|
1088
|
+
stageTool = undefined;
|
|
719
1089
|
if (tokens >= budget)
|
|
720
1090
|
throw new Error("Token budget exhausted");
|
|
721
1091
|
}
|
|
@@ -728,6 +1098,24 @@ export class AgentRunner {
|
|
|
728
1098
|
return { sessionId, text: lastText, status: "turns-exceeded", usage: usageTotal };
|
|
729
1099
|
}
|
|
730
1100
|
catch (error) {
|
|
1101
|
+
const timedOut = silentTimedOut
|
|
1102
|
+
? { kind: "first_token", ms: firstTokenMs, attempts: Math.max(1, silentAttempts) }
|
|
1103
|
+
: combined.aborted && isTimeoutReason(combined.reason)
|
|
1104
|
+
? { kind: "run", ms: runTimeoutMs }
|
|
1105
|
+
: undefined;
|
|
1106
|
+
if (timedOut && !(combined.aborted && !isTimeoutReason(combined.reason))) {
|
|
1107
|
+
const failure = new RunTimeoutError({
|
|
1108
|
+
...timedOut,
|
|
1109
|
+
...(modelId ? { model: modelId } : {}),
|
|
1110
|
+
...(providerName ? { provider: providerName } : {}),
|
|
1111
|
+
stage,
|
|
1112
|
+
...(stage === "tool" && stageTool ? { tool: stageTool } : {}),
|
|
1113
|
+
...(requests <= 1 ? { firstRequest: true } : {}),
|
|
1114
|
+
});
|
|
1115
|
+
emit("run_failed", { error: failure.message, code: "timeout", timeout: failure.info });
|
|
1116
|
+
finish("failed", failure.message);
|
|
1117
|
+
throw failure;
|
|
1118
|
+
}
|
|
731
1119
|
// On cancellation report the abort reason, not the transport's secondary error.
|
|
732
1120
|
const cause = combined.aborted ? (combined.reason ?? error) : error;
|
|
733
1121
|
const message = cause instanceof Error ? cause.message : String(cause);
|