@sema-agent/core 5.10.0 → 5.11.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/CHANGELOG.md +23 -0
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/agents/subagent.js +6 -2
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/openai.js +1 -1
- package/dist/core/background-agent-store.d.ts +2 -1
- package/dist/core/background-agent-store.js +1 -0
- package/dist/core/checkpoint-store.d.ts +2 -0
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/runner/assemble-result.js +9 -0
- package/dist/core/runner/prepare-task.d.ts +4 -0
- package/dist/core/runner/prepare-task.js +66 -28
- package/dist/core/runner/runtask.js +42 -15
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +24 -9
- package/dist/core/runner/turn-attachments.d.ts +2 -0
- package/dist/core/runner/turn-attachments.js +14 -5
- package/dist/core/task-registry-agent.d.ts +3 -0
- package/dist/core/task-registry-agent.js +9 -2
- package/dist/core/task-registry-shared.d.ts +1 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/prompts/default.d.ts +1 -0
- package/dist/prompts/default.js +3 -0
- package/dist/tools/fs/fs-bash.js +4 -1
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +7 -4
- package/dist/tools/web.d.ts +21 -1
- package/dist/tools/web.js +126 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.11.0 — 2026-08-04
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
- **The restart-loop suspend cap counts CONSECUTIVE NO-PROGRESS suspends, not total approvals.** The cap's stated purpose is catching a restart-prone model re-issuing the same gated call forever — yet it counted distinct approved-and-executed gates identically, so under a doctrine that asks per call (`shellGate:"classify"` + out-of-root reads) a fully cooperating approver was killed on the 6th legitimate Yes (`suspend.loop`, default `maxSuspends: 5`). The chain base now resets the moment the resume leg starts EXECUTING the approved pending call (an execution that then errors still consumed the approval — it reset the chain); a park whose approved call never ran keeps accumulating. **Consumer flips**: checkpoint `suspendCount` restarts at 1 after each executed approval — probes pinning a monotonically increasing `suspendCount` across approve-execute cycles red; `maxSuspends` now bounds no-progress chains, never the total number of approvals a run may receive.
|
|
8
|
+
- **`SubagentErrorKind` closed set gains a sixth member: `"governance"`.** A FAILED child whose `errorCode` carries the `usage.*` prefix (deployment usage window refused/exhausted) now classifies as `governance` with `retryable: false` — it is DEFERRED-retryable: re-issuing NOW gets the same refusal, but the refusal lifts at the card's `retryAfterMs`. Previously these folded into `logic` ("re-delegating unchanged won't help"), which told a scheduler to abandon a subtask that merely needed to wait. The transient retry set (`rate_limit`/`overloaded`/`timeout`/`network` → `retryable: true`) is untouched. **Consumer flips**: switches over the closed five-member kind set add a `governance` arm; the completed agent card now pairs `error_kind: "governance"` with `retryAfterMs` on window-exhausted children.
|
|
9
|
+
|
|
10
|
+
- **Deferred-tool activation no longer rewrites the provider tools block — `toolMaterializeStrategy` defaults to `"static"`.** Tools serialize FIRST in the provider prompt-cache hierarchy, so the old activation swap (placeholder → full schema via setTools) invalidated the entire conversation cache — 29% of benchmarked trials paid an average ~31k tokens per break. Under the new default the placeholder bytes persist for the whole run; the full schema reaches the model through the ToolSearch result text (existing carrier) and engine-side validation against the REAL schema is unchanged. Ruled GO by a live A/B on the weakest BYOM model (15/15 argument instances correct, three scenario classes incl. nested/optional params). `"swap"` is the explicit opt-back (spec or `SEMA_TOOL_MATERIALIZE_STRATEGY`, invalid env value refused loudly as `config.tool_materialize_invalid`). **Consumer flips**: probes pinning the post-activation full schema in the tools block red — the activation announcement frames and genuine roster changes (MCP refresh) are unchanged.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A shell-gated ask persists the live shellGate doctrine on its risk descriptor.** Field forensics could not answer "was `classify` or `always` live when this gate fired" from any persisted artifact. The mint now stamps `RiskDescriptor.shellGateDoctrine` (`"classify" | "always"`, present only on shell-gated asks — `"off"` never mints) so the checkpoint row self-reports its provenance. Additive optional field.
|
|
15
|
+
- **`TaskSpec.additionalReadDirectories` — a read-only fence widening.** Extra directories the READ faces admit and nothing else: the `shellGate:"classify"` read boundary (a provably read-only command like `head <dir>/settings.json` auto-allows instead of consulting the approver on every call — the interactive kill shape this exists for), plus read_file/grep/glob/repo_map and `bash_readonly` containment. The write faces (edit_file/write_file/notebook_edit) refuse these directories exactly as before — a read grant never silently becomes a write grant (`additionalDirectories` remains the read+write widening). Same canonicalization/fail-closed-skip rules as `additionalDirectories` (the skip trace gains an optional `field` discriminator); announced in `# Environment` as `Additional read-only directories:`; inherited down the delegation tree on the same seat as `additionalDirectories`. Additive — absent ⇒ byte-identical behavior.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **WebFetch stops laundering emptiness into authority (grounding contract).** A shell page (tiny extracted text, or near-zero text ratio) now gets a TRUSTED grounding verdict outside the untrusted fence, the full extracted text appended for verification, and additive `details.grounding{level,textChars,bytes,textRatio}`; an incomplete body is called a retrieved prefix. The summarizer input budget derives from the summary model's window (flat 100k retired; trimmed input disclosed structurally and in the rendered head; NaN model metadata refused loudly). Recovery prose names Bash/curl only when reachable in THIS run. The summary prompt gains a grounding clause (report absence rather than infer; a filename is only evidence about naming) ahead of the byte-fixed CC guidelines tail.
|
|
20
|
+
- **A truncated-tool-arguments disclosure names the request output cap when one was armed** (`request output cap in effect: N tokens`) on both brain lanes — a cut mid-arguments at the cap read as a generic provider error with no budget clue.
|
|
21
|
+
- **agentStream's birth window is steerable**: an immediate `steer()` after `agentStream()` waits (bounded) for the loop's first prompt and lands on turn 1; a finished run keeps the honest refusal.
|
|
22
|
+
- **A long-window zero-byte Bash timeout names its two honest readings** (stalled before producing output / holding it in a block buffer); short windows and abort arms stay bare.
|
|
23
|
+
- **The background faces carry the governance wait hint.** A `usage.window_exhausted` background child's settle dropped `retryAfterMs` — every bg face (poll text, poll details, notification-adjacent durable row) said `retryable:false` with no way to know when the refusal lifts, while the sync report card carried the hint. The classification family gains its fourth seat end-to-end: settle mint → registry row → durable archival row → poll `details.retryAfterMs` → the poll text clause (`(error_kind: governance, retryable: false, retry_after_ms: N)`); a revive clears it with its three siblings. Additive on every face.
|
|
24
|
+
- **The `suspend.loop` terminal diagnoses the every-approval-was-Yes kill.** When every recorded gate decision on the run was ALLOW, the message now names the shape (a gate that keeps asking × an approver that keeps approving consumed the suspend allowance on legitimate work) and the remedies (a live `onAsk` parks nothing; keep benign commands inside the read boundary; raise `maxSuspends` for approval-heavy tasks) instead of reporting a bare restart-loop mystery.
|
|
25
|
+
|
|
3
26
|
## 5.10.0 — 2026-08-04
|
|
4
27
|
|
|
5
28
|
### Added
|
|
@@ -34,7 +34,7 @@ export interface SubagentWorktreeIsolation {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function createSubagentWorktreeHelper(baseEnv: ExecutionEnv, repoRoot: string): SubagentWorktreeIsolation;
|
|
36
36
|
export declare const REPORT_FIELD_MAX = 300;
|
|
37
|
-
export type SubagentErrorKind = "rate_limit" | "overloaded" | "timeout" | "network" | "logic";
|
|
37
|
+
export type SubagentErrorKind = "rate_limit" | "overloaded" | "timeout" | "network" | "logic" | "governance";
|
|
38
38
|
export declare function classifySubagentError(child: {
|
|
39
39
|
status: string;
|
|
40
40
|
errorCode?: string;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -229,8 +229,10 @@ export function classifySubagentError(child) {
|
|
|
229
229
|
? "timeout"
|
|
230
230
|
: code === "network" || code === "server"
|
|
231
231
|
? "network"
|
|
232
|
-
: "
|
|
233
|
-
|
|
232
|
+
: code !== undefined && code.startsWith("usage.")
|
|
233
|
+
? "governance"
|
|
234
|
+
: "logic";
|
|
235
|
+
return { errorKind, retryable: errorKind !== "logic" && errorKind !== "governance" };
|
|
234
236
|
}
|
|
235
237
|
export function completedAgentCard(child, extras) {
|
|
236
238
|
return {
|
|
@@ -1502,6 +1504,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1502
1504
|
...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
|
|
1503
1505
|
...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
|
|
1504
1506
|
...(ctx.additionalDirectories !== undefined ? { additionalDirectories: [...ctx.additionalDirectories] } : {}),
|
|
1507
|
+
...(ctx.additionalReadDirectories !== undefined ? { additionalReadDirectories: [...ctx.additionalReadDirectories] } : {}),
|
|
1505
1508
|
...(ctx.envFacts !== undefined ? { envFacts: { ...ctx.envFacts } } : {}),
|
|
1506
1509
|
...(ctx.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: ctx.getApiKeyAndHeaders } : {}),
|
|
1507
1510
|
enableBlockedReport: true,
|
|
@@ -2567,6 +2570,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2567
2570
|
...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : unparkedPauseReason ?? child.errorMessage ?? String(child.status) } : {}),
|
|
2568
2571
|
...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
|
|
2569
2572
|
...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
|
|
2573
|
+
...(failedBg && child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
|
|
2570
2574
|
}) ??
|
|
2571
2575
|
(abort.signal.aborted ? "killed" : ok ? "completed" : "failed");
|
|
2572
2576
|
if (!bgRetain && reviveRow === undefined && !(await bgRowConfirmed())) {
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -558,7 +558,7 @@ export function createAnthropicBrain(config = {}) {
|
|
|
558
558
|
const noUsableContent = toolCalls.length === 0 && !anyText;
|
|
559
559
|
const toolErrorParts = [];
|
|
560
560
|
if (malformed.length > 0) {
|
|
561
|
-
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, stop_reason="${stopReason ?? "?"}"): ${malformed.join("; ")}`);
|
|
561
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, stop_reason="${stopReason ?? "?"}"${sentMaxTokens !== undefined ? `; request output cap in effect: ${sentMaxTokens} tokens — a cut mid-arguments commonly means the cap was hit` : ""}): ${malformed.join("; ")}`);
|
|
562
562
|
}
|
|
563
563
|
if (unnamed.length > 0) {
|
|
564
564
|
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (stop_reason="${stopReason ?? "?"}"): ${unnamed.join("; ")}`);
|
package/dist/brain/openai.js
CHANGED
|
@@ -526,7 +526,7 @@ export function createOpenAIBrain(config = {}) {
|
|
|
526
526
|
}
|
|
527
527
|
const toolErrorParts = [];
|
|
528
528
|
if (malformed.length > 0) {
|
|
529
|
-
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, finish_reason="${finishReason ?? "?"}"): ${malformed.join("; ")}`);
|
|
529
|
+
toolErrorParts.push(`tool call argument(s) not valid JSON (likely truncated, finish_reason="${finishReason ?? "?"}"${sentMaxTokens !== undefined ? `; request output cap in effect: ${sentMaxTokens} tokens — a cut mid-arguments commonly means the cap was hit` : ""}): ${malformed.join("; ")}`);
|
|
530
530
|
}
|
|
531
531
|
if (unnamed.length > 0) {
|
|
532
532
|
toolErrorParts.push(`tool call(s) arrived with no tool name and cannot be executed (finish_reason="${finishReason ?? "?"}"): ${unnamed.join("; ")}`);
|
|
@@ -40,13 +40,14 @@ export interface BackgroundAgentRecord {
|
|
|
40
40
|
errorCode?: string;
|
|
41
41
|
errorRetryable?: boolean;
|
|
42
42
|
errorKind?: string;
|
|
43
|
+
errorRetryAfterMs?: number;
|
|
43
44
|
resultIsPartial?: boolean;
|
|
44
45
|
recentSteps?: SubagentStep[];
|
|
45
46
|
editedFiles?: SubagentEditedFile[];
|
|
46
47
|
usage?: BackgroundAgentUsage;
|
|
47
48
|
rev: number;
|
|
48
49
|
}
|
|
49
|
-
export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
|
|
50
|
+
export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "errorRetryAfterMs", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
|
|
50
51
|
export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
|
|
51
52
|
export interface BackgroundAgentRowSummary {
|
|
52
53
|
handle: string;
|
|
@@ -24,6 +24,7 @@ export interface RiskDescriptor {
|
|
|
24
24
|
shell?: boolean;
|
|
25
25
|
};
|
|
26
26
|
toolName: string;
|
|
27
|
+
shellGateDoctrine?: "classify" | "always";
|
|
27
28
|
summary?: string;
|
|
28
29
|
touchedPaths?: string[];
|
|
29
30
|
}
|
|
@@ -39,6 +40,7 @@ export declare function buildRiskDescriptor(input: {
|
|
|
39
40
|
args: unknown;
|
|
40
41
|
safety?: SafetyAxis;
|
|
41
42
|
shellGated?: boolean;
|
|
43
|
+
shellGateDoctrine?: "classify" | "always";
|
|
42
44
|
}): RiskDescriptor;
|
|
43
45
|
export type CheckpointGate = {
|
|
44
46
|
kind: "human";
|
|
@@ -115,6 +115,7 @@ export function buildRiskDescriptor(input) {
|
|
|
115
115
|
severity: riskSeverity(axes),
|
|
116
116
|
axes,
|
|
117
117
|
toolName,
|
|
118
|
+
...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
|
|
118
119
|
...(summary !== undefined ? { summary } : {}),
|
|
119
120
|
...(touchedPaths !== undefined ? { touchedPaths } : {}),
|
|
120
121
|
};
|
|
@@ -86,6 +86,15 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
86
86
|
status = "failed";
|
|
87
87
|
errorCode = "suspend.loop";
|
|
88
88
|
errorMessage = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
|
|
89
|
+
const gates = stats.humanReview?.gates ?? [];
|
|
90
|
+
if (gates.length > 0 && gates.every((g) => g.decision === "allow")) {
|
|
91
|
+
errorMessage +=
|
|
92
|
+
`. Diagnosis: all ${gates.length} recorded gate decision(s) on this run were ALLOW — a gate that keeps ` +
|
|
93
|
+
`asking combined with an approver that keeps approving consumes the suspend allowance on legitimate ` +
|
|
94
|
+
`work. Remedies: answer asks at a LIVE onAsk (a synchronous allow parks nothing and consumes no ` +
|
|
95
|
+
`suspend), keep provably-benign commands inside the read boundary so the classifier auto-allows them, ` +
|
|
96
|
+
`or raise maxSuspends for genuinely approval-heavy tasks.`;
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
99
|
else if (flags.threw) {
|
|
91
100
|
status = "failed";
|
|
@@ -121,6 +121,7 @@ export interface Prepared {
|
|
|
121
121
|
} | undefined;
|
|
122
122
|
activeTools: Set<string>;
|
|
123
123
|
deferredToolNames?: ReadonlySet<string>;
|
|
124
|
+
toolMaterializeStatic: boolean;
|
|
124
125
|
memoryEngineSession?: {
|
|
125
126
|
engine: MemoryEngine;
|
|
126
127
|
handle: MemorySessionHandle;
|
|
@@ -134,6 +135,9 @@ export interface Prepared {
|
|
|
134
135
|
scope?: string;
|
|
135
136
|
restoreMode?: "snapshot" | "park_only";
|
|
136
137
|
};
|
|
138
|
+
suspendProgressRef: {
|
|
139
|
+
executedApproved: boolean;
|
|
140
|
+
};
|
|
137
141
|
reviewRef: {
|
|
138
142
|
token?: CheckpointToken;
|
|
139
143
|
gate?: CheckpointGate;
|
|
@@ -319,6 +319,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
319
319
|
throw e;
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
if (spec.toolMaterializeStrategy !== undefined && spec.toolMaterializeStrategy !== "swap" && spec.toolMaterializeStrategy !== "static") {
|
|
323
|
+
const e = new Error(`toolMaterializeStrategy must be "swap" or "static" (got ${JSON.stringify(spec.toolMaterializeStrategy)}).`);
|
|
324
|
+
e.code = "config.tool_materialize_invalid";
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
|
|
328
|
+
const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
|
|
329
|
+
`lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
|
|
330
|
+
`called. Use "swap", or leave deferSelfResolve on.`);
|
|
331
|
+
e.code = "config.tool_materialize_unreachable";
|
|
332
|
+
throw e;
|
|
333
|
+
}
|
|
322
334
|
if (resume === undefined && spec.objective.trim().length === 0) {
|
|
323
335
|
const e = new Error("TaskSpec.objective is empty — a task needs an instruction (an empty user message is rejected by strict model endpoints and would fail every later request of the session).");
|
|
324
336
|
e.code = "config.empty_objective";
|
|
@@ -977,6 +989,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
977
989
|
alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
|
|
978
990
|
promptProfile,
|
|
979
991
|
...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
|
|
992
|
+
...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
|
|
980
993
|
...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
|
|
981
994
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
982
995
|
parentCwd: taskRootPath,
|
|
@@ -1151,6 +1164,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1151
1164
|
gates: resume?.priorHumanReview?.gates ? [...resume.priorHumanReview.gates] : [],
|
|
1152
1165
|
};
|
|
1153
1166
|
const priorSuspendCount = resume?.priorSuspendCount ?? 0;
|
|
1167
|
+
const suspendProgressRef = { executedApproved: false };
|
|
1168
|
+
const suspendChainBase = () => (suspendProgressRef.executedApproved ? 0 : priorSuspendCount);
|
|
1154
1169
|
const maxSuspends = spec.maxSuspends ?? deps.maxSuspends ?? DEFAULT_MAX_SUSPENDS;
|
|
1155
1170
|
const maxSlices = spec.resourceSuspend?.maxSlices;
|
|
1156
1171
|
const priorLedger = resume?.priorLedger;
|
|
@@ -1334,34 +1349,40 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1334
1349
|
const callIssuedAtRef = {};
|
|
1335
1350
|
const memoryWriteGateRef = {};
|
|
1336
1351
|
const additionalRootsCanonical = [];
|
|
1352
|
+
const additionalReadRootsCanonical = [];
|
|
1337
1353
|
let attachmentRootCanonical;
|
|
1338
1354
|
if (handsEnabled) {
|
|
1339
1355
|
const rootRaw = taskRootPath;
|
|
1340
1356
|
const canon = await executionEnv.canonicalPath(rootRaw);
|
|
1341
1357
|
const rootCanonical = canon.ok ? canon.value : rootRaw;
|
|
1342
1358
|
attachmentRootCanonical = rootCanonical;
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1359
|
+
const canonicalizeExtraDirs = async (dirs, field, sink) => {
|
|
1360
|
+
for (const dir of dirs ?? []) {
|
|
1361
|
+
if (!dir || !dir.trim())
|
|
1362
|
+
continue;
|
|
1363
|
+
const c = await executionEnv.canonicalPath(dir);
|
|
1364
|
+
if (c.ok) {
|
|
1365
|
+
sink.push(c.value);
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
deps.onError?.(new Error(`${field} entry skipped (cannot canonicalize): ${dir}`), {
|
|
1369
|
+
phase: "config",
|
|
1370
|
+
sessionId,
|
|
1371
|
+
});
|
|
1372
|
+
emitTrace(deps.tracer, () => ({
|
|
1373
|
+
kind: "config.additional_directory_skipped",
|
|
1374
|
+
version: 1,
|
|
1375
|
+
taskId: hostTaskId,
|
|
1376
|
+
entry: dir,
|
|
1377
|
+
...(field !== "additionalDirectories" ? { field } : {}),
|
|
1378
|
+
reason: `${c.error.code}: ${c.error.message}`,
|
|
1379
|
+
ts: Date.now(),
|
|
1380
|
+
}));
|
|
1381
|
+
}
|
|
1363
1382
|
}
|
|
1364
|
-
}
|
|
1383
|
+
};
|
|
1384
|
+
await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
|
|
1385
|
+
await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
|
|
1365
1386
|
if (spec.envFacts?.scratchpadDir) {
|
|
1366
1387
|
let c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
|
|
1367
1388
|
if (!c.ok && c.error.code === "not_found") {
|
|
@@ -1412,6 +1433,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1412
1433
|
}
|
|
1413
1434
|
const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
|
|
1414
1435
|
...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
|
|
1436
|
+
...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
|
|
1415
1437
|
includeShell: handsIncludeShell,
|
|
1416
1438
|
readOnly: handsReadOnly,
|
|
1417
1439
|
...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
|
|
@@ -1453,7 +1475,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1453
1475
|
irreversibilityTier.set("Bash", shellGate === "always" ? "always" : "maybe");
|
|
1454
1476
|
irreversibleTools.add("Bash");
|
|
1455
1477
|
const shellReadBoundary = () => ({
|
|
1456
|
-
roots: [rootCanonical, ...additionalRootsCanonical],
|
|
1478
|
+
roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
|
|
1457
1479
|
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1458
1480
|
});
|
|
1459
1481
|
if (shellGate === "classify")
|
|
@@ -1777,6 +1799,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1777
1799
|
if (additionalRootsCanonical.length > 0) {
|
|
1778
1800
|
envFacts.additionalDirectories = [...additionalRootsCanonical];
|
|
1779
1801
|
}
|
|
1802
|
+
if (additionalReadRootsCanonical.length > 0) {
|
|
1803
|
+
envFacts.additionalReadDirectories = [...additionalReadRootsCanonical];
|
|
1804
|
+
}
|
|
1780
1805
|
try {
|
|
1781
1806
|
const probe = await executionEnv.exec('uname -s; uname -r; (git rev-parse --is-inside-work-tree 2>/dev/null || echo false); (git symbolic-ref --short -q HEAD 2>/dev/null || echo "HEAD (detached)"); (git rev-parse --show-toplevel 2>/dev/null || echo); (test -n "$(git status --porcelain 2>/dev/null | head -1)" && echo dirty || echo clean); (s=$(ps -p $$ -o comm= 2>/dev/null); s=${s##*/}; echo "${s#-}"); (test "$(git rev-parse --git-dir 2>/dev/null)" != "$(git rev-parse --git-common-dir 2>/dev/null)" && echo linked || echo main); (pwd -P 2>/dev/null || pwd)', { cwd: envFacts.cwd, timeout: 10 });
|
|
1782
1807
|
if (probe.ok && probe.value.exitCode === 0) {
|
|
@@ -2021,6 +2046,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2021
2046
|
.filter((s) => s.status === "failed")
|
|
2022
2047
|
.map((s) => ({ name: inlineUntrusted(s.name, 160), ...(s.error !== undefined ? { error: inlineUntrusted(s.error, 240) } : {}) }));
|
|
2023
2048
|
let toolsDeltaRef;
|
|
2049
|
+
let toolMaterializeStatic = false;
|
|
2024
2050
|
if (deferred.size > 0 || failedMcpServers.length > 0) {
|
|
2025
2051
|
toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
|
|
2026
2052
|
}
|
|
@@ -2097,8 +2123,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2097
2123
|
};
|
|
2098
2124
|
offloadReachableToolsRef.current = callableToolNames;
|
|
2099
2125
|
let toolSearch;
|
|
2126
|
+
const envStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
|
|
2127
|
+
if (envStrategy !== undefined && envStrategy !== "swap" && envStrategy !== "static") {
|
|
2128
|
+
const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(envStrategy)}).`);
|
|
2129
|
+
e.code = "config.tool_materialize_invalid";
|
|
2130
|
+
throw e;
|
|
2131
|
+
}
|
|
2132
|
+
const materializeStatic = (spec.toolMaterializeStrategy ?? envStrategy ?? "static") === "static" && spec.deferSelfResolve !== false;
|
|
2133
|
+
toolMaterializeStatic = materializeStatic;
|
|
2100
2134
|
const buildToolList = (active) => {
|
|
2101
|
-
const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
|
|
2135
|
+
const list = tools.map((t) => (deferred.has(t.name) && (materializeStatic || !active.has(t.name)) ? placeholders.get(t.name) : t));
|
|
2102
2136
|
list.push(toolSearch);
|
|
2103
2137
|
return list;
|
|
2104
2138
|
};
|
|
@@ -2139,6 +2173,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2139
2173
|
listingRide: (newly) => listingRideRef.current?.(newly),
|
|
2140
2174
|
mountedNames: callableToolNames,
|
|
2141
2175
|
directCallEnabled: spec.deferSelfResolve !== false,
|
|
2176
|
+
...(materializeStatic
|
|
2177
|
+
? { staticSchemaFor: (name) => tools.find((t) => t.name === name)?.parameters }
|
|
2178
|
+
: {}),
|
|
2142
2179
|
serializeActivation,
|
|
2143
2180
|
});
|
|
2144
2181
|
harnessTools = buildToolList(activeTools);
|
|
@@ -3083,7 +3120,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3083
3120
|
}
|
|
3084
3121
|
return false;
|
|
3085
3122
|
}
|
|
3086
|
-
if (suspendLoopCapHit(
|
|
3123
|
+
if (suspendLoopCapHit(suspendChainBase(), maxSuspends, " for a plan_review (likely a resume/restart loop)."))
|
|
3087
3124
|
return false;
|
|
3088
3125
|
const leafId = await session.getLeafId();
|
|
3089
3126
|
if (!leafId) {
|
|
@@ -3131,7 +3168,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3131
3168
|
createdAt: mintedAt,
|
|
3132
3169
|
suspendedAt: now(),
|
|
3133
3170
|
deadline: mintedAt + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
|
|
3134
|
-
suspendCount:
|
|
3171
|
+
suspendCount: suspendChainBase() + 1,
|
|
3135
3172
|
humanReview: humanReviewRef.count > 0
|
|
3136
3173
|
? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
|
|
3137
3174
|
: undefined,
|
|
@@ -3192,7 +3229,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3192
3229
|
if (abortController.signal.aborted) {
|
|
3193
3230
|
return undefined;
|
|
3194
3231
|
}
|
|
3195
|
-
if (suspendLoopCapHit(
|
|
3232
|
+
if (suspendLoopCapHit(suspendChainBase(), maxSuspends, ` for tool "${req.toolName}" — likely a resume/restart loop.`))
|
|
3196
3233
|
return undefined;
|
|
3197
3234
|
if (remoteEnv !== undefined) {
|
|
3198
3235
|
if (hasBackgroundShell(remoteEnv))
|
|
@@ -3215,6 +3252,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3215
3252
|
args: postHookArgs,
|
|
3216
3253
|
safety,
|
|
3217
3254
|
shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
|
|
3255
|
+
...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
|
|
3218
3256
|
});
|
|
3219
3257
|
gate =
|
|
3220
3258
|
safety !== undefined
|
|
@@ -3275,7 +3313,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3275
3313
|
humanReview: humanReviewRef.count > 0
|
|
3276
3314
|
? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
|
|
3277
3315
|
: undefined,
|
|
3278
|
-
suspendCount:
|
|
3316
|
+
suspendCount: suspendChainBase() + 1,
|
|
3279
3317
|
resourceLedger: approvalLedger,
|
|
3280
3318
|
sourceTaskId: sessionId,
|
|
3281
3319
|
...(spec.principal ? { principal: spec.principal } : {}),
|
|
@@ -3636,7 +3674,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3636
3674
|
: undefined;
|
|
3637
3675
|
overheadState.promptChars = systemPrompt.length;
|
|
3638
3676
|
const preparedHolder = {};
|
|
3639
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3677
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3640
3678
|
const prepared = buildPrepared();
|
|
3641
3679
|
preparedHolder.current = prepared;
|
|
3642
3680
|
return prepared;
|
|
@@ -541,6 +541,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
541
541
|
: {}),
|
|
542
542
|
...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
|
|
543
543
|
...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
|
|
544
|
+
...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
|
|
544
545
|
...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
|
|
545
546
|
...(rs.attach.agentListingOn && prepared.agentListing !== undefined
|
|
546
547
|
? {
|
|
@@ -556,7 +557,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
556
557
|
...(mcpDropped !== undefined ? { mcpDroppedTools: mcpDropped } : {}),
|
|
557
558
|
}));
|
|
558
559
|
if (pendingTools !== undefined || mcpToolsDelta !== undefined) {
|
|
559
|
-
const exact = renderToolsDelta({
|
|
560
|
+
const exact = renderToolsDelta({
|
|
561
|
+
...(pendingTools !== undefined ? { added: pendingTools } : {}),
|
|
562
|
+
...(prepared.toolMaterializeStatic ? { staticFace: true } : {}),
|
|
563
|
+
...(mcpToolsDelta ?? {}),
|
|
564
|
+
});
|
|
560
565
|
if (exact !== undefined && due.some((a) => a.source === "tools_delta" && a.body === exact)) {
|
|
561
566
|
const ref = prepared.toolsDeltaRef;
|
|
562
567
|
if (pendingTools !== undefined)
|
|
@@ -1338,6 +1343,7 @@ export class Runner {
|
|
|
1338
1343
|
return Promise.race([p, timeout]).finally(() => clearTimeout(t));
|
|
1339
1344
|
};
|
|
1340
1345
|
let reapHandle;
|
|
1346
|
+
let steerChain = Promise.resolve();
|
|
1341
1347
|
const notifyRef = {};
|
|
1342
1348
|
const manualCompactRef = { requested: false, waiters: [] };
|
|
1343
1349
|
const drainManualCompactWaiters = (outcome) => {
|
|
@@ -1501,24 +1507,41 @@ export class Runner {
|
|
|
1501
1507
|
return suggestionsDone.catch(() => []);
|
|
1502
1508
|
},
|
|
1503
1509
|
steer: async (text, options) => {
|
|
1504
|
-
if (resultValue)
|
|
1505
|
-
throw steeringError("the task has already finished");
|
|
1506
|
-
const h = handle ?? (await orTimeout(ready));
|
|
1507
|
-
if (!h)
|
|
1508
|
-
throw steeringError("the task is not running");
|
|
1509
1510
|
if (options?.trusted && sanitizeUntrustedText(text) !== text) {
|
|
1510
1511
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1511
1512
|
}
|
|
1512
1513
|
const payload = options?.trusted ? formatHookFeedback(text) : text;
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
if (
|
|
1518
|
-
throw steeringError("the task is
|
|
1514
|
+
const deliver = async () => {
|
|
1515
|
+
if (resultValue)
|
|
1516
|
+
throw steeringError("the task has already finished");
|
|
1517
|
+
const h = handle ?? (await orTimeout(ready));
|
|
1518
|
+
if (!h)
|
|
1519
|
+
throw steeringError("the task is not running");
|
|
1520
|
+
try {
|
|
1521
|
+
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1522
|
+
return;
|
|
1519
1523
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1524
|
+
catch (e) {
|
|
1525
|
+
if (!(e instanceof Error && e.code === "invalid_state"))
|
|
1526
|
+
throw e;
|
|
1527
|
+
}
|
|
1528
|
+
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1529
|
+
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1530
|
+
try {
|
|
1531
|
+
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
catch (e2) {
|
|
1535
|
+
if (!(e2 instanceof Error && e2.code === "invalid_state"))
|
|
1536
|
+
throw e2;
|
|
1537
|
+
}
|
|
1538
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1539
|
+
}
|
|
1540
|
+
throw steeringError("the task is no longer running");
|
|
1541
|
+
};
|
|
1542
|
+
const p = steerChain.then(deliver);
|
|
1543
|
+
steerChain = p.then(() => undefined, () => undefined);
|
|
1544
|
+
return p;
|
|
1522
1545
|
},
|
|
1523
1546
|
notify: async (input, opts) => {
|
|
1524
1547
|
const notifyError = (msg, code) => {
|
|
@@ -1753,7 +1776,8 @@ export class Runner {
|
|
|
1753
1776
|
}
|
|
1754
1777
|
}
|
|
1755
1778
|
}
|
|
1756
|
-
|
|
1779
|
+
const loopLatch = { ended: false };
|
|
1780
|
+
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch });
|
|
1757
1781
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
1758
1782
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
1759
1783
|
if (resume &&
|
|
@@ -2705,9 +2729,11 @@ export class Runner {
|
|
|
2705
2729
|
}));
|
|
2706
2730
|
}
|
|
2707
2731
|
}
|
|
2732
|
+
loopLatch.ended = true;
|
|
2708
2733
|
abortedLive = prepared.abortController.signal.aborted;
|
|
2709
2734
|
}
|
|
2710
2735
|
catch (err) {
|
|
2736
|
+
loopLatch.ended = true;
|
|
2711
2737
|
if (errorCodeOf(err) === "resume.tool_unavailable") {
|
|
2712
2738
|
await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
2713
2739
|
await settleTeardownLeg(() => prepared.a2a?.dispose(), "a2a.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
@@ -3512,6 +3538,7 @@ export class Runner {
|
|
|
3512
3538
|
}
|
|
3513
3539
|
const args = resolvedArgs;
|
|
3514
3540
|
onExecuteStart?.(pendingAction.toolCallId);
|
|
3541
|
+
prepared.suspendProgressRef.executedApproved = true;
|
|
3515
3542
|
let res;
|
|
3516
3543
|
try {
|
|
3517
3544
|
res = await tool.execute(pendingAction.toolCallId, args, prepared.abortController.signal);
|
|
@@ -54,4 +54,5 @@ export declare function createToolSearchTool(opts: {
|
|
|
54
54
|
mountedNames?: () => ReadonlySet<string>;
|
|
55
55
|
directCallEnabled?: boolean;
|
|
56
56
|
serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
|
|
57
|
+
staticSchemaFor?: (name: string) => TSchema | undefined;
|
|
57
58
|
}): AgentTool;
|
|
@@ -80,7 +80,7 @@ export function createPlaceholderTool(info, direct) {
|
|
|
80
80
|
};
|
|
81
81
|
const invalidArgumentsRejection = (target, params, schemaJson, ride) => {
|
|
82
82
|
const text = `Invalid arguments for \`${sn}\`: ${formatZodValidationError(target.parameters, params)} ` +
|
|
83
|
-
`\`${sn}\` is now active — its full parameter schema is below
|
|
83
|
+
`\`${sn}\` is now active — its full parameter schema is below; use it for this and later calls. ` +
|
|
84
84
|
`Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schemaJson}`;
|
|
85
85
|
return {
|
|
86
86
|
content: ride === undefined || ride === "" ? [{ type: "text", text }] : [{ type: "text", text }, { type: "text", text: ride }],
|
|
@@ -236,11 +236,15 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
236
236
|
return [...names];
|
|
237
237
|
}
|
|
238
238
|
export function createToolSearchTool(opts) {
|
|
239
|
-
const { registry, active, rematerialize, listingRide, mountedNames } = opts;
|
|
239
|
+
const { registry, active, rematerialize, listingRide, mountedNames, staticSchemaFor } = opts;
|
|
240
240
|
const directCallEnabled = opts.directCallEnabled !== false;
|
|
241
241
|
const activationPosture = directCallEnabled
|
|
242
|
-
?
|
|
243
|
-
"
|
|
242
|
+
? (staticSchemaFor !== undefined
|
|
243
|
+
? "Most tools start as name-only placeholders to keep requests small; activating one here returns its full " +
|
|
244
|
+
"parameter schema in the result (the tools list keeps the compact placeholder entry). "
|
|
245
|
+
: "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
|
|
246
|
+
"parameter schema. ") +
|
|
247
|
+
"Until you have that schema you cannot reliably form a call, so activate a tool rather " +
|
|
244
248
|
"than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
|
|
245
249
|
"When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
|
|
246
250
|
'with query "select:<name>". '
|
|
@@ -265,7 +269,9 @@ export function createToolSearchTool(opts) {
|
|
|
265
269
|
"a bare tool name — activates that tool directly. " +
|
|
266
270
|
"Activate every tool you expect to need in one call (select accepts a comma-separated list) " +
|
|
267
271
|
"rather than one at a time. " +
|
|
268
|
-
|
|
272
|
+
(staticSchemaFor !== undefined
|
|
273
|
+
? "Activation returns each tool's full parameter schema in this result — call the tool directly with arguments matching it."
|
|
274
|
+
: "Activated tools become callable with their full parameters on your next turn."),
|
|
269
275
|
parameters: Type.Object({
|
|
270
276
|
query: Type.Optional(Type.String({
|
|
271
277
|
description: 'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.',
|
|
@@ -329,11 +335,20 @@ export function createToolSearchTool(opts) {
|
|
|
329
335
|
const lines = matched.map((n) => {
|
|
330
336
|
const info = registry.get(n);
|
|
331
337
|
const tag = newly.includes(n) ? "activated" : "already active";
|
|
332
|
-
|
|
338
|
+
const base = `- ${safeName(n)} (${tag})${info ? ` — ${info.hint}` : ""}`;
|
|
339
|
+
if (staticSchemaFor === undefined)
|
|
340
|
+
return base;
|
|
341
|
+
const schema = staticSchemaFor(n);
|
|
342
|
+
const json = schema === undefined ? undefined : renderSchemaForModel(schema);
|
|
343
|
+
return json === undefined ? base : `${base}\n parameters: ${json}`;
|
|
333
344
|
});
|
|
334
|
-
const head =
|
|
335
|
-
?
|
|
336
|
-
|
|
345
|
+
const head = staticSchemaFor !== undefined
|
|
346
|
+
? newly.length > 0
|
|
347
|
+
? `Activated ${newly.length} tool(s) — call them directly with arguments matching the parameter schemas below (the tools list keeps compact placeholder entries):`
|
|
348
|
+
: "These tools are already active — call them directly; their parameter schemas are repeated below:"
|
|
349
|
+
: newly.length > 0
|
|
350
|
+
? `Activated ${newly.length} tool(s); they are now available with full parameters — call them directly:`
|
|
351
|
+
: "These tools are already active — call them directly:";
|
|
337
352
|
return {
|
|
338
353
|
content: `${head}\n${lines.join("\n")}${missingNote}${ride !== undefined ? `\n\n${ride}` : ""}`,
|
|
339
354
|
details: {
|
|
@@ -100,6 +100,7 @@ export interface AttachmentInputs {
|
|
|
100
100
|
}>;
|
|
101
101
|
backgroundTasks?: ReadonlyArray<BackgroundTaskSnapshot>;
|
|
102
102
|
newTools?: readonly string[];
|
|
103
|
+
newToolsStaticFace?: boolean;
|
|
103
104
|
mcpToolsDelta?: McpToolsDeltaFacts;
|
|
104
105
|
agentListing?: ReadonlyArray<AgentListingEntry>;
|
|
105
106
|
agentToolName?: string;
|
|
@@ -148,6 +149,7 @@ export interface McpToolsDeltaFacts {
|
|
|
148
149
|
}
|
|
149
150
|
export declare function renderToolsDelta(input: {
|
|
150
151
|
added?: readonly string[];
|
|
152
|
+
staticFace?: boolean;
|
|
151
153
|
} & McpToolsDeltaFacts): string | undefined;
|
|
152
154
|
export declare const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
|
|
153
155
|
export declare const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
|
|
@@ -168,7 +168,11 @@ export function collectDueAttachments(state, inp) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
if (inp.config.toolsDelta) {
|
|
171
|
-
const body = renderToolsDelta({
|
|
171
|
+
const body = renderToolsDelta({
|
|
172
|
+
...(inp.newTools !== undefined ? { added: inp.newTools } : {}),
|
|
173
|
+
...(inp.newToolsStaticFace === true ? { staticFace: true } : {}),
|
|
174
|
+
...(inp.mcpToolsDelta ?? {}),
|
|
175
|
+
});
|
|
172
176
|
if (body !== undefined)
|
|
173
177
|
(out ??= []).push({ source: "tools_delta", body });
|
|
174
178
|
}
|
|
@@ -386,15 +390,20 @@ export function renderToolsDelta(input) {
|
|
|
386
390
|
const blocks = [];
|
|
387
391
|
const added = input.added ?? [];
|
|
388
392
|
if (added.length > 0) {
|
|
389
|
-
blocks.push(
|
|
390
|
-
"directly
|
|
393
|
+
blocks.push((input.staticFace === true
|
|
394
|
+
? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
|
|
395
|
+
"provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
|
|
396
|
+
: "The following deferred tools are now available. Their full schemas are loaded — call them " +
|
|
397
|
+
"directly like any other tool:\n") +
|
|
391
398
|
added.map((n) => `- ${n}`).join("\n"));
|
|
392
399
|
}
|
|
393
400
|
const readded = input.readded ?? [];
|
|
394
401
|
if (readded.length > 0) {
|
|
395
402
|
blocks.push(`${readded.length} deferred tool${readded.length === 1 ? " is" : "s are"} available again (MCP server reconnected — ` +
|
|
396
|
-
`names announced earlier in this conversation): ${groupByMcpServer(readded)}.
|
|
397
|
-
|
|
403
|
+
`names announced earlier in this conversation): ${groupByMcpServer(readded)}. ` +
|
|
404
|
+
(input.staticFace === true
|
|
405
|
+
? `The tools list keeps compact placeholder entries — re-run ToolSearch ("select:<name>") if you need their current parameter schemas.`
|
|
406
|
+
: `Their schemas are loaded again — call them directly.`));
|
|
398
407
|
}
|
|
399
408
|
const removed = input.removed ?? [];
|
|
400
409
|
if (removed.length > 0) {
|
|
@@ -69,6 +69,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
|
|
|
69
69
|
errorCode?: string;
|
|
70
70
|
retryable?: boolean;
|
|
71
71
|
errorKind?: string;
|
|
72
|
+
retryAfterMs?: number;
|
|
72
73
|
stoppedBy?: StopSource;
|
|
73
74
|
seq?: number;
|
|
74
75
|
cycle?: number;
|
|
@@ -105,6 +106,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
|
|
|
105
106
|
errorCode?: string;
|
|
106
107
|
retryable?: boolean;
|
|
107
108
|
errorKind?: string;
|
|
109
|
+
retryAfterMs?: number;
|
|
108
110
|
}): "completed" | "failed" | "killed" | undefined;
|
|
109
111
|
export declare function noteBackgroundAgentActivityLane(core: DurableAgentCore, id: string, now?: number): void;
|
|
110
112
|
export declare function reapStaleSessionBackgroundAgentsLane(core: DurableAgentCore, staleMs: number, now?: number, onTerminal?: (note: () => void) => void): number;
|
|
@@ -137,6 +139,7 @@ export interface AgentPollDetailsInput {
|
|
|
137
139
|
error?: string;
|
|
138
140
|
errorCode?: string;
|
|
139
141
|
errorRetryable?: boolean;
|
|
142
|
+
errorRetryAfterMs?: number;
|
|
140
143
|
resultIsPartial?: boolean;
|
|
141
144
|
completionId?: string;
|
|
142
145
|
}
|
|
@@ -713,6 +713,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
713
713
|
handle.errorRetryable = outcome.retryable;
|
|
714
714
|
if (outcome.errorKind !== undefined)
|
|
715
715
|
handle.errorKind = outcome.errorKind;
|
|
716
|
+
if (outcome.retryAfterMs !== undefined)
|
|
717
|
+
handle.errorRetryAfterMs = outcome.retryAfterMs;
|
|
716
718
|
}
|
|
717
719
|
handle.updatedAt = Date.now();
|
|
718
720
|
if (outcome.seq !== undefined)
|
|
@@ -731,6 +733,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
|
|
|
731
733
|
...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
732
734
|
...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
|
|
733
735
|
...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
|
|
736
|
+
...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
|
|
734
737
|
}, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
735
738
|
return outcome.status;
|
|
736
739
|
}
|
|
@@ -857,6 +860,7 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
|
|
|
857
860
|
handle.errorCode = undefined;
|
|
858
861
|
handle.errorRetryable = undefined;
|
|
859
862
|
handle.errorKind = undefined;
|
|
863
|
+
handle.errorRetryAfterMs = undefined;
|
|
860
864
|
handle.resultIsPartial = undefined;
|
|
861
865
|
handle.stopSource = undefined;
|
|
862
866
|
handle.completionId = undefined;
|
|
@@ -1052,6 +1056,7 @@ export function buildAgentPollDetails(input) {
|
|
|
1052
1056
|
...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
|
|
1053
1057
|
...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
|
|
1054
1058
|
...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
|
|
1059
|
+
...(failed && input.errorRetryAfterMs !== undefined ? { retryAfterMs: input.errorRetryAfterMs } : {}),
|
|
1055
1060
|
...(input.resultIsPartial === true ? { partial_result: true } : {}),
|
|
1056
1061
|
...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
|
|
1057
1062
|
};
|
|
@@ -1070,7 +1075,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1070
1075
|
};
|
|
1071
1076
|
}
|
|
1072
1077
|
const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
|
|
1073
|
-
? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
|
|
1078
|
+
? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable}${row.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${row.errorRetryAfterMs}` : ""})`
|
|
1074
1079
|
: "";
|
|
1075
1080
|
const body = `status: ${row.status}
|
|
1076
1081
|
${row.error ? `error: ${row.error}${kindClause}
|
|
@@ -1087,6 +1092,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1087
1092
|
...(row.error !== undefined ? { error: row.error } : {}),
|
|
1088
1093
|
...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
|
|
1089
1094
|
...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
|
|
1095
|
+
...(row.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: row.errorRetryAfterMs } : {}),
|
|
1090
1096
|
...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
|
|
1091
1097
|
...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
|
|
1092
1098
|
}),
|
|
@@ -1126,7 +1132,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1126
1132
|
const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
|
|
1127
1133
|
const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
|
|
1128
1134
|
const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
|
|
1129
|
-
? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
|
|
1135
|
+
? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable}${handle.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${handle.errorRetryAfterMs}` : ""})`
|
|
1130
1136
|
: "";
|
|
1131
1137
|
const body = running
|
|
1132
1138
|
? oneShot === true
|
|
@@ -1149,6 +1155,7 @@ ${resultText}` : "(no result text)"}`;
|
|
|
1149
1155
|
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1150
1156
|
...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
|
|
1151
1157
|
...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
|
|
1158
|
+
...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
|
|
1152
1159
|
...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
|
|
1153
1160
|
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1154
1161
|
}),
|
|
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
139
139
|
errorCode?: string;
|
|
140
140
|
errorRetryable?: boolean;
|
|
141
141
|
errorKind?: string;
|
|
142
|
+
errorRetryAfterMs?: number;
|
|
142
143
|
resultIsPartial?: boolean;
|
|
143
144
|
stopSource?: StopSource;
|
|
144
145
|
stoppedBy?: StopSource;
|
|
@@ -141,6 +141,7 @@ export declare class TaskRegistry {
|
|
|
141
141
|
errorCode?: string;
|
|
142
142
|
retryable?: boolean;
|
|
143
143
|
errorKind?: string;
|
|
144
|
+
retryAfterMs?: number;
|
|
144
145
|
stoppedBy?: StopSource;
|
|
145
146
|
seq?: number;
|
|
146
147
|
cycle?: number;
|
|
@@ -180,6 +181,7 @@ export declare class TaskRegistry {
|
|
|
180
181
|
errorCode?: string;
|
|
181
182
|
retryable?: boolean;
|
|
182
183
|
errorKind?: string;
|
|
184
|
+
retryAfterMs?: number;
|
|
183
185
|
}): "completed" | "failed" | "killed" | undefined;
|
|
184
186
|
unmarkRetainedContinuation(id: string): void;
|
|
185
187
|
attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
|
package/dist/core/trace.d.ts
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -109,6 +109,7 @@ export interface ToolExecuteContext {
|
|
|
109
109
|
alwaysLoadTools?: readonly string[];
|
|
110
110
|
promptProfile?: "simple" | "classic";
|
|
111
111
|
additionalDirectories?: readonly string[];
|
|
112
|
+
additionalReadDirectories?: readonly string[];
|
|
112
113
|
envFacts?: TaskSpec["envFacts"];
|
|
113
114
|
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
114
115
|
activeSkillScope?: () => readonly unknown[];
|
|
@@ -297,6 +298,7 @@ export interface TaskSpec {
|
|
|
297
298
|
tools?: ToolSpec[];
|
|
298
299
|
excludeTools?: string[];
|
|
299
300
|
deferTools?: string[];
|
|
301
|
+
toolMaterializeStrategy?: "swap" | "static";
|
|
300
302
|
alwaysLoadTools?: string[];
|
|
301
303
|
deferSelfResolve?: boolean;
|
|
302
304
|
promptProfile?: "simple" | "classic";
|
|
@@ -325,6 +327,7 @@ export interface TaskSpec {
|
|
|
325
327
|
checkpointStore?: import("./checkpoint-store.js").CheckpointStore | null;
|
|
326
328
|
handsReadOnly?: boolean;
|
|
327
329
|
additionalDirectories?: string[];
|
|
330
|
+
additionalReadDirectories?: string[];
|
|
328
331
|
enablePlanMode?: boolean;
|
|
329
332
|
interactiveTools?: boolean;
|
|
330
333
|
enableFork?: boolean;
|
package/dist/prompts/default.js
CHANGED
|
@@ -237,6 +237,9 @@ export function buildEnvironmentContext(facts) {
|
|
|
237
237
|
if (facts.additionalDirectories && facts.additionalDirectories.length > 0) {
|
|
238
238
|
lines.push(`Additional working directories: ${facts.additionalDirectories.map((d) => inlineUntrusted(d)).join(", ")}`);
|
|
239
239
|
}
|
|
240
|
+
if (facts.additionalReadDirectories && facts.additionalReadDirectories.length > 0) {
|
|
241
|
+
lines.push(`Additional read-only directories: ${facts.additionalReadDirectories.map((d) => inlineUntrusted(d)).join(", ")}`);
|
|
242
|
+
}
|
|
240
243
|
if (facts.isGitRepo !== undefined)
|
|
241
244
|
lines.push(`Is a git repository: ${facts.isGitRepo ? "yes" : "no"}`);
|
|
242
245
|
if (facts.gitBranch)
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -296,9 +296,12 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
296
296
|
const captured = (pStdout ? `--- partial stdout ---\n${pStdout}` : "") +
|
|
297
297
|
(pStderr ? `${pStdout ? "\n" : ""}--- partial stderr ---\n${pStderr}` : "");
|
|
298
298
|
const zeroOutput = captured.length === 0;
|
|
299
|
+
const zeroOutputHint = res.error.code === "timeout" && timeout >= 60
|
|
300
|
+
? ` — the process ran the full ${timeout}s without writing to its stdio; it may have been stalled before producing output, or holding it in a block buffer`
|
|
301
|
+
: "";
|
|
299
302
|
const body = !zeroOutput
|
|
300
303
|
? `\n${delimitUntrusted("partial command output", captured)}`
|
|
301
|
-
: `\n(no output was produced before the cutoff)`;
|
|
304
|
+
: `\n(no output was produced before the cutoff${zeroOutputHint})`;
|
|
302
305
|
const overflowNote = cutOverflowFile !== undefined ? shellRecoveryHint(cutOverflowFile, readOnly) : "";
|
|
303
306
|
return {
|
|
304
307
|
content: `Error (${toolName}): ${headline}${body}${overflowNote}`,
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./fs-bash.js";
|
|
|
13
13
|
import { type CwdRef, type ReadImageDownsamplerOption } from "./fs-shared.js";
|
|
14
14
|
export interface HandsToolkitOptions {
|
|
15
15
|
additionalRoots?: readonly string[];
|
|
16
|
+
additionalReadRoots?: readonly string[];
|
|
16
17
|
includeShell?: boolean;
|
|
17
18
|
readOnly?: boolean;
|
|
18
19
|
bashReadonlyAllow?: readonly string[];
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -16,7 +16,10 @@ import { createEditFileTool, createWriteFileTool, createNotebookEditTool } from
|
|
|
16
16
|
import { createGrepTool, createGlobTool } from "./fs-search-tools.js";
|
|
17
17
|
import { createBashTool, createBashReadonlyTool, createEnvTaskOutputTool, createEnvTaskStopTool } from "./fs-bash.js";
|
|
18
18
|
export function createHandsToolkit(env, readFileState, rootCanonical, opts = {}) {
|
|
19
|
-
const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, } = opts;
|
|
19
|
+
const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, additionalReadRoots, } = opts;
|
|
20
|
+
const readFaceRoots = additionalReadRoots === undefined || additionalReadRoots.length === 0
|
|
21
|
+
? additionalRoots
|
|
22
|
+
: [...(additionalRoots ?? []), ...additionalReadRoots];
|
|
20
23
|
const cwdRef = opts.cwdRef ?? { current: rootCanonical };
|
|
21
24
|
const bgReadRegistry = opts.taskRegistry;
|
|
22
25
|
const bgOutputReadExemption = bgReadRegistry === undefined
|
|
@@ -27,18 +30,18 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
27
30
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
28
31
|
}, env);
|
|
29
32
|
const tools = [
|
|
30
|
-
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef,
|
|
33
|
+
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption),
|
|
31
34
|
];
|
|
32
35
|
if (!readOnly) {
|
|
33
36
|
tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite));
|
|
34
37
|
}
|
|
35
|
-
tools.push(createGrepTool(env, rootCanonical,
|
|
38
|
+
tools.push(createGrepTool(env, rootCanonical, readFaceRoots), createGlobTool(env, rootCanonical, readFaceRoots), createRepoMapTool(env, rootCanonical, readFaceRoots));
|
|
36
39
|
if (includeShell) {
|
|
37
40
|
tools.push(readOnly
|
|
38
41
|
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
|
|
39
42
|
...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
|
|
40
43
|
...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
|
|
41
|
-
...(
|
|
44
|
+
...(readFaceRoots !== undefined ? { additionalRoots: readFaceRoots } : {}),
|
|
42
45
|
})
|
|
43
46
|
: createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
|
|
44
47
|
taskRegistry: opts.taskRegistry,
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -9,17 +9,37 @@ export interface WebFetchConfig {
|
|
|
9
9
|
summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
10
10
|
text: string;
|
|
11
11
|
truncated?: boolean;
|
|
12
|
+
inputTruncated?: boolean;
|
|
13
|
+
inputChars?: number;
|
|
14
|
+
usedChars?: number;
|
|
12
15
|
}>;
|
|
13
16
|
userAgent?: string;
|
|
14
17
|
}
|
|
18
|
+
export declare const WEBFETCH_GROUNDING_MIN_TEXT_CHARS = 200;
|
|
19
|
+
export interface WebFetchGrounding {
|
|
20
|
+
level: "ok" | "low";
|
|
21
|
+
textChars: number;
|
|
22
|
+
bytes: number;
|
|
23
|
+
textRatio: number;
|
|
24
|
+
}
|
|
15
25
|
export declare function htmlToText(html: string): string;
|
|
16
26
|
export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
|
|
17
27
|
export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
|
|
18
28
|
export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
|
|
19
29
|
export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
|
|
20
|
-
export declare
|
|
30
|
+
export declare const WEBFETCH_SUMMARY_GROUNDING_CLAUSE: string;
|
|
31
|
+
export declare const WEBFETCH_SUMMARY_INPUT_HEADROOM = 0.8;
|
|
32
|
+
export declare const WEBFETCH_SUMMARY_MIN_CONTENT = 4000;
|
|
33
|
+
export declare function resolveSummaryInputChars(model: Model, override?: number): number;
|
|
34
|
+
export interface WebFetchSummarizerOptions {
|
|
35
|
+
maxContentChars?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function createWebFetchSummarizer(brain: Brain, model: Model, options?: WebFetchSummarizerOptions): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
21
38
|
text: string;
|
|
22
39
|
truncated?: boolean;
|
|
40
|
+
inputTruncated?: boolean;
|
|
41
|
+
inputChars?: number;
|
|
42
|
+
usedChars?: number;
|
|
23
43
|
}>;
|
|
24
44
|
export interface WebSearchConfig {
|
|
25
45
|
search: (query: string, signal?: AbortSignal, opts?: {
|
package/dist/tools/web.js
CHANGED
|
@@ -25,6 +25,25 @@ function resolveWebMaxBytes(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
export const WEBFETCH_GROUNDING_MIN_TEXT_CHARS = 200;
|
|
29
|
+
const GROUNDING_MIN_TEXT_RATIO = 0.01;
|
|
30
|
+
const GROUNDING_RATIO_MAX_TEXT_CHARS = 2_000;
|
|
31
|
+
const GROUNDING_ECHO_MAX_CHARS = GROUNDING_RATIO_MAX_TEXT_CHARS + 100;
|
|
32
|
+
const GROUNDING_SHELL_MIN_BYTES = 500;
|
|
33
|
+
function assessGrounding(text, bytes) {
|
|
34
|
+
const textChars = text.replace(/\s+/g, " ").trim().length;
|
|
35
|
+
const textRatio = bytes > 0 ? (textChars / bytes) : 0;
|
|
36
|
+
const low = textChars < WEBFETCH_GROUNDING_MIN_TEXT_CHARS ||
|
|
37
|
+
(textChars < GROUNDING_RATIO_MAX_TEXT_CHARS && bytes > 0 && textRatio < GROUNDING_MIN_TEXT_RATIO);
|
|
38
|
+
return { level: low ? "low" : "ok", textChars, bytes, textRatio: Math.round(textRatio * 1000) / 1000 };
|
|
39
|
+
}
|
|
40
|
+
function recoveryToolRuledOut(ctx, toolName) {
|
|
41
|
+
if (ctx.excludeTools?.includes(toolName) ?? false)
|
|
42
|
+
return true;
|
|
43
|
+
const deferred = ctx.deferTools?.includes(toolName) ?? false;
|
|
44
|
+
const pinnedInline = ctx.alwaysLoadTools?.includes(toolName) ?? false;
|
|
45
|
+
return deferred && !pinnedInline;
|
|
46
|
+
}
|
|
28
47
|
const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
29
48
|
const ERROR_BODY_EXCERPT_CHARS = 2048;
|
|
30
49
|
const ERROR_BODY_CONVERT_MAX_CHARS = 64 * 1024;
|
|
@@ -471,10 +490,14 @@ export function webFetchToolSpec(config = {}) {
|
|
|
471
490
|
: bodyCut
|
|
472
491
|
? `\n\n[WebFetch: ${cutPhrase} — the ${bodyBytes.length} bytes are a partial prefix received before the cutoff, NOT the object's full size]`
|
|
473
492
|
: "";
|
|
493
|
+
const shellRecoveryRuledOut = recoveryToolRuledOut(ctx, "Bash") || ctx.handsReadOnly === true;
|
|
494
|
+
const recoveryLine = shellRecoveryRuledOut
|
|
495
|
+
? "Retrieve it outside this tool — this run has no shell capability that can download it to a file."
|
|
496
|
+
: `Download and inspect it with Bash instead, e.g.: curl -L -o /tmp/download "${parsed.toString()}"`;
|
|
474
497
|
return {
|
|
475
498
|
content: `Error (WebFetch): binary content detected (${kind}${mt && signature ? `, content-type: ${mt}` : ""}, ` +
|
|
476
499
|
`${bodyBytes.length} bytes) — the body was NOT added to the context (it does not decode as text). ` +
|
|
477
|
-
|
|
500
|
+
recoveryLine +
|
|
478
501
|
binaryStateNote,
|
|
479
502
|
details: {
|
|
480
503
|
type: "web-fetch",
|
|
@@ -503,8 +526,11 @@ export function webFetchToolSpec(config = {}) {
|
|
|
503
526
|
raw = raw.slice(0, maxBytes);
|
|
504
527
|
}
|
|
505
528
|
const text = /html/i.test(contentType) || /^\s*</.test(raw) ? htmlToText(raw) : raw;
|
|
529
|
+
const grounding = assessGrounding(text, bodyBytes ? bodyBytes.length : raw.length);
|
|
506
530
|
let out = text;
|
|
507
531
|
let summaryTruncated = false;
|
|
532
|
+
let summaryApplied = false;
|
|
533
|
+
let summaryInputNote;
|
|
508
534
|
let note;
|
|
509
535
|
if (prompt && bodyCut) {
|
|
510
536
|
note =
|
|
@@ -516,13 +542,24 @@ export function webFetchToolSpec(config = {}) {
|
|
|
516
542
|
const summarized = await config.summarize(text, prompt, ctx.signal);
|
|
517
543
|
if (typeof summarized === "string") {
|
|
518
544
|
out = summarized;
|
|
545
|
+
summaryApplied = true;
|
|
519
546
|
}
|
|
520
547
|
else {
|
|
521
548
|
out = summarized.text;
|
|
549
|
+
summaryApplied = true;
|
|
522
550
|
if (summarized.truncated) {
|
|
523
551
|
summaryTruncated = true;
|
|
524
552
|
note = `[note: the summary below is INCOMPLETE — the summarizer hit its output limit before finishing]`;
|
|
525
553
|
}
|
|
554
|
+
if (summarized.inputTruncated) {
|
|
555
|
+
const sizes = summarized.usedChars !== undefined && summarized.inputChars !== undefined
|
|
556
|
+
? ` (only the first ${summarized.usedChars} of ${summarized.inputChars} characters were read)`
|
|
557
|
+
: "";
|
|
558
|
+
summaryInputNote =
|
|
559
|
+
`[note: the summary below covers only the BEGINNING of the page${sizes} — the page exceeded what the ` +
|
|
560
|
+
`summarizer could feed its model, so the rest was never read. Absence of something from the summary ` +
|
|
561
|
+
`does NOT mean it is absent from the page.]`;
|
|
562
|
+
}
|
|
526
563
|
}
|
|
527
564
|
}
|
|
528
565
|
catch (e) {
|
|
@@ -537,12 +574,44 @@ export function webFetchToolSpec(config = {}) {
|
|
|
537
574
|
`[note: summarization unavailable in this deployment — raw page content follows; ` +
|
|
538
575
|
`the requested analysis ("${inlineUntrusted(prompt, 120)}") was NOT applied]`;
|
|
539
576
|
}
|
|
577
|
+
const bodyIncomplete = truncationNote !== "" || bodyCut !== undefined;
|
|
578
|
+
let groundingNote;
|
|
579
|
+
let sourceEcho = "";
|
|
580
|
+
if (grounding.level === "low") {
|
|
581
|
+
const shellShape = /html/i.test(contentType) && grounding.bytes >= GROUNDING_SHELL_MIN_BYTES
|
|
582
|
+
? bodyIncomplete
|
|
583
|
+
? " The retrieved bytes are almost entirely markup/script — which is either a client-rendered shell or " +
|
|
584
|
+
"simply the head of a document whose content sits past the retrieved prefix; a larger byte limit (or a " +
|
|
585
|
+
"completed transfer) would distinguish the two."
|
|
586
|
+
: " The retrieved bytes are almost entirely markup/script, which is the shape of a page whose content is " +
|
|
587
|
+
"loaded by JavaScript after the document — this tool does not run JavaScript, so that content was never present."
|
|
588
|
+
: "";
|
|
589
|
+
const nothingExtractable = grounding.textChars === 0;
|
|
590
|
+
const sourceLabel = bodyIncomplete ? "extracted text of the retrieved prefix" : "complete extracted page text";
|
|
591
|
+
const head = `[WebFetch grounding: LOW — the ${bodyIncomplete ? "retrieved prefix" : "page"} yielded ` +
|
|
592
|
+
(nothingExtractable ? "NO extractable text at all" : `only ${grounding.textChars} characters of extractable text`) +
|
|
593
|
+
` out of ${grounding.bytes} retrieved bytes`;
|
|
594
|
+
groundingNote = summaryApplied
|
|
595
|
+
? `${head}. ` +
|
|
596
|
+
(nothingExtractable
|
|
597
|
+
? `There is no source text at all, so every specific claim in the summary below is unsourced — do not use it as a factual source.`
|
|
598
|
+
: `That may be all this URL serves, or it may be a page whose content is not present in the fetched bytes — this tool ` +
|
|
599
|
+
`cannot tell the two apart, so the summary below is NOT usable as a source on its own: check every specific claim ` +
|
|
600
|
+
`in it against the ${sourceLabel} reproduced after it.`) +
|
|
601
|
+
shellShape +
|
|
602
|
+
`]`
|
|
603
|
+
: `${head}; the content below is ${bodyIncomplete ? "all the retrieved prefix yielded" : "all of it"}.${shellShape}]`;
|
|
604
|
+
if (summaryApplied && !nothingExtractable) {
|
|
605
|
+
sourceEcho = `\n\n${delimitUntrusted(`WebFetch ${parsed.hostname} — ${sourceLabel}`, text.replace(/\s+/g, " ").trim(), GROUNDING_ECHO_MAX_CHARS)}`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
540
608
|
const fenced = delimitUntrusted(`WebFetch ${parsed.hostname}`, out);
|
|
541
|
-
const
|
|
609
|
+
const headNotes = [groundingNote, note, summaryInputNote].filter((n) => n !== undefined);
|
|
610
|
+
const withNote = headNotes.length > 0 ? `${headNotes.join("\n\n")}\n\n${fenced}` : fenced;
|
|
542
611
|
const partialNote = bodyCut
|
|
543
612
|
? `\n\n[WebFetch: ${cutPhrase} — the content above is PARTIAL: ${bodyBytes?.length ?? 0} bytes were received before the cutoff and the tail is missing. Treat absent information as unfetched, not absent from the source.]`
|
|
544
613
|
: "";
|
|
545
|
-
const modelText = withNote + truncationNote + partialNote;
|
|
614
|
+
const modelText = withNote + truncationNote + partialNote + sourceEcho;
|
|
546
615
|
const RESULT_PREVIEW_CHARS = 8_000;
|
|
547
616
|
return {
|
|
548
617
|
content: modelText,
|
|
@@ -554,7 +623,9 @@ export function webFetchToolSpec(config = {}) {
|
|
|
554
623
|
bytes: bodyBytes ? bodyBytes.length : raw.length,
|
|
555
624
|
result: out.length > RESULT_PREVIEW_CHARS ? `${out.slice(0, RESULT_PREVIEW_CHARS)}\n…[${out.length - RESULT_PREVIEW_CHARS} chars truncated — full text in the tool output]` : out,
|
|
556
625
|
durationMs: Date.now() - startedAt,
|
|
557
|
-
...(truncationNote || summaryTruncated ? { truncated: true } : {}),
|
|
626
|
+
...(truncationNote || summaryTruncated || summaryInputNote ? { truncated: true } : {}),
|
|
627
|
+
grounding,
|
|
628
|
+
...(summaryInputNote ? { summaryInputTruncated: true } : {}),
|
|
558
629
|
...transferStateDetails,
|
|
559
630
|
},
|
|
560
631
|
};
|
|
@@ -570,12 +641,50 @@ export const WEBFETCH_SUMMARY_GUIDELINES = `Provide a concise response based onl
|
|
|
570
641
|
` - Use quotation marks for exact language from articles; any language outside of the quotation should never be word-for-word the same.\n` +
|
|
571
642
|
` - You are not a lawyer and never comment on the legality of your own prompts and responses.\n` +
|
|
572
643
|
` - Never produce or reproduce exact song lyrics.\n`;
|
|
573
|
-
export
|
|
644
|
+
export const WEBFETCH_SUMMARY_GROUNDING_CLAUSE = `Ground every statement in the content above:\n` +
|
|
645
|
+
` - If the content does not contain what was asked for, say exactly that and describe what the content IS instead. Never fill the gap with general knowledge or plausible inference.\n` +
|
|
646
|
+
` - If the content is a listing (file names, links, titles) rather than the data itself, a name is evidence only about naming — do not conclude from names alone that the underlying resource does or does not contain something; report what the listing shows and what would have to be opened to answer.\n`;
|
|
647
|
+
export const WEBFETCH_SUMMARY_INPUT_HEADROOM = 0.8;
|
|
648
|
+
export const WEBFETCH_SUMMARY_MIN_CONTENT = 4_000;
|
|
649
|
+
const SUMMARY_PROMPT_OVERHEAD_TOKENS = 1_000;
|
|
650
|
+
const SUMMARY_PROMPT_ALLOWANCE_CHARS = 2_000;
|
|
651
|
+
const SUMMARY_MIN_CONTENT_PER_CALL = 1_024;
|
|
652
|
+
function invalidSummaryBudget(message) {
|
|
653
|
+
const e = new Error(message);
|
|
654
|
+
e.code = "config.web_summary_max_content_invalid";
|
|
655
|
+
return e;
|
|
656
|
+
}
|
|
657
|
+
export function resolveSummaryInputChars(model, override) {
|
|
658
|
+
if (override !== undefined) {
|
|
659
|
+
if (!Number.isFinite(override) || override < 1) {
|
|
660
|
+
throw invalidSummaryBudget(`WebFetch summarizer maxContentChars must be a finite number of characters >= 1 (got ${String(override)})`);
|
|
661
|
+
}
|
|
662
|
+
return Math.floor(override);
|
|
663
|
+
}
|
|
664
|
+
const window = model.contextTokens ?? model.contextWindow;
|
|
665
|
+
const charsPerToken = model.charsPerToken ?? 4;
|
|
666
|
+
for (const [name, value] of [
|
|
667
|
+
["contextTokens/contextWindow", window],
|
|
668
|
+
["maxTokens", model.maxTokens],
|
|
669
|
+
["charsPerToken", charsPerToken],
|
|
670
|
+
]) {
|
|
671
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
672
|
+
throw invalidSummaryBudget(`WebFetch summarizer cannot size its input: model "${model.id}" declares a non-finite or non-positive ${name} (got ${String(value)})`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const reservedOutputTokens = Math.min(model.maxTokens, Math.floor(window / 4));
|
|
676
|
+
const inputTokens = window - reservedOutputTokens - SUMMARY_PROMPT_OVERHEAD_TOKENS;
|
|
677
|
+
const derived = Math.floor(inputTokens * charsPerToken * WEBFETCH_SUMMARY_INPUT_HEADROOM);
|
|
678
|
+
return Math.min(WEBFETCH_SUMMARY_MAX_CONTENT, Math.max(WEBFETCH_SUMMARY_MIN_CONTENT, derived));
|
|
679
|
+
}
|
|
680
|
+
export function createWebFetchSummarizer(brain, model, options = {}) {
|
|
681
|
+
const maxContentChars = resolveSummaryInputChars(model, options.maxContentChars);
|
|
574
682
|
return async (content, prompt, signal) => {
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
683
|
+
const promptOverflow = Math.max(0, prompt.length - SUMMARY_PROMPT_ALLOWANCE_CHARS);
|
|
684
|
+
const budget = promptOverflow === 0 ? maxContentChars : Math.max(SUMMARY_MIN_CONTENT_PER_CALL, maxContentChars - promptOverflow);
|
|
685
|
+
const inputTruncated = content.length > budget;
|
|
686
|
+
const truncated = inputTruncated ? content.slice(0, budget) + "\n\n[Content truncated due to length...]" : content;
|
|
687
|
+
const userPrompt = `\nWeb page content:\n---\n${truncated}\n---\n\n${prompt}\n\n` + WEBFETCH_SUMMARY_GROUNDING_CLAUSE + "\n" + WEBFETCH_SUMMARY_GUIDELINES;
|
|
579
688
|
const context = { messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] };
|
|
580
689
|
const msg = brain.complete
|
|
581
690
|
? await brain.complete(model, context, { signal })
|
|
@@ -590,7 +699,14 @@ export function createWebFetchSummarizer(brain, model) {
|
|
|
590
699
|
.trim();
|
|
591
700
|
if (!text)
|
|
592
701
|
throw new Error("summarizer returned no text");
|
|
593
|
-
|
|
702
|
+
const outputTruncated = msg.stopReason === "length" || msg.partialFinalized === true;
|
|
703
|
+
if (!outputTruncated && !inputTruncated)
|
|
704
|
+
return text;
|
|
705
|
+
return {
|
|
706
|
+
text,
|
|
707
|
+
...(outputTruncated ? { truncated: true } : {}),
|
|
708
|
+
...(inputTruncated ? { inputTruncated: true, inputChars: content.length, usedChars: budget } : {}),
|
|
709
|
+
};
|
|
594
710
|
};
|
|
595
711
|
}
|
|
596
712
|
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|