@sema-agent/core 5.3.0 → 5.4.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 +18 -0
- package/dist/brain/anthropic.js +12 -2
- package/dist/brain/media-degrade.d.ts +14 -0
- package/dist/brain/media-degrade.js +30 -0
- package/dist/brain/openai.js +9 -11
- package/dist/brain/timeout.d.ts +14 -0
- package/dist/brain/timeout.js +114 -0
- package/dist/core/exec-output-tail.d.ts +0 -1
- package/dist/core/exec-output-tail.js +0 -6
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +10 -2
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/task-registry-agent.js +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +0 -1
- package/dist/engine/execution-env/node-execution-env.js +12 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/fs-read.js +5 -3
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +1 -0
- package/dist/tools/web.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.4.0 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
_The external-audit dunning batch closes end-to-end (small cases + all three cleared A-group debts), and the engine grows its outer brain-call guardrail. All additive or louder; major stays locked at 5._
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- webfetch: the tool card follows the wired capability (a mount without a summarizer no longer advertises `prompt` in prose or schema), and an error response stops silently eating the caller's `prompt` (the error leg now carries the same "was NOT applied" disclosure family, naming the HTTP status). Error bodies are deliberately NOT fed to the summarizer (a 404 page dressed up as "the requested analysis" would be worse than the raw dump) — pinned as a negative control.
|
|
10
|
+
- Background-agent poll projection: stop-attribution rides whenever the ROW carries it — the stale-running reaper settles rows as `failed` with `stoppedBy:"system"`, and the previous killed-only gate dropped that fact, making a reaped row indistinguishable from a natural failure. A row without the field still projects nothing.
|
|
11
|
+
- Brains: an ERROR tool result now discloses the media it had to drop (`[N images omitted — …]`, both wire formats) instead of silently presenting an attachment-free error; the vision-degrade predicates and placeholder wording collapse into one construction point (`media-degrade.ts`).
|
|
12
|
+
- Image read cap: the refusal for an oversized image without a downsampler now names `sharp` (declared in `optionalDependencies`) and the 64MB ceiling it unlocks — the cap itself is unchanged.
|
|
13
|
+
|
|
14
|
+
- Background-command spool (RB-235 close-out): a LIVE writer's spool file is append-only — the reclaim truncate now waits for the writer to be terminal, closing the stat→truncate race window that silently destroyed whole buffered write blocks (external audit reproduced 8192-multiple losses on 5.3.0; the fix's own red-first run recorded 65536/720896/294912-byte ledger gaps). Cost, stated honestly: a live writer past the hard cap no longer has physical space reclaimed until it dies (the reader stays bounded and byte-accounted as before); the file is reclaimed wholesale at writer terminal. The live-rotation disclosure marker is retired with the race (structurally impossible now).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `STALE_RUNNING_REAP_ATTRIBUTION` (exported): the reaper's settle wording reaches the package root, so an out-of-repo store twin can import the attribution instead of value-copying it against the contract kit (downstream pickup request).
|
|
19
|
+
- `TaskSpec.limits.brainCallGuardrailMs` + `RunnerDeps.brainCallGuardrailMs` (RB-458): an OUTER per-call guardrail on how long the engine waits for one brain call — default 1 hour, `false`/`0` switches it off, invalid values refuse loudly at prepare. Independent of brain cooperation: expiry aborts the brain's signal AND stops the engine's own waiting at every await point, so a custom Brain that ignores `options.signal` can no longer wedge a run forever (previously the de-facto bound was an accidental property of the bundled fetch stack). A run stopped this way terminates loud with `TaskResult.errorCode = "brain.call_guardrail_timeout"`; walltime-budgeted runs keep their own (usually tighter) clocks — the two race and the earlier one wins.
|
|
20
|
+
|
|
3
21
|
## 5.3.0 (2026-08-03)
|
|
4
22
|
|
|
5
23
|
_The A2A arc: client+server legs land end-to-end (all additive), the workflow-size notice lane closes its channel, and three naming/containment defects fix. Major stays locked at 5._
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
7
7
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
8
|
+
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
9
|
import { ANTHROPIC_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
9
10
|
import { isThinkingLevel, reasoningBudgetShare, resolveEffort } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
@@ -37,7 +38,7 @@ function toUserBlocks(content, supportsVision = true) {
|
|
|
37
38
|
blocks.push({ type: "image", source: { type: "base64", media_type: c.mimeType, data: c.data } });
|
|
38
39
|
else {
|
|
39
40
|
omitted++;
|
|
40
|
-
blocks.push({ type: "text", text:
|
|
41
|
+
blocks.push({ type: "text", text: IMAGE_OMITTED_NO_VISION });
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -46,7 +47,7 @@ function toUserBlocks(content, supportsVision = true) {
|
|
|
46
47
|
return blocks;
|
|
47
48
|
}
|
|
48
49
|
function toAnthropicMessages(ctx, model) {
|
|
49
|
-
const supportsVision = model
|
|
50
|
+
const supportsVision = modelSupportsVision(model);
|
|
50
51
|
const raw = [];
|
|
51
52
|
let pendingDocs = [];
|
|
52
53
|
const flushDocs = () => {
|
|
@@ -112,6 +113,15 @@ function toAnthropicMessages(ctx, model) {
|
|
|
112
113
|
inner.push({ type: "text", text: note });
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
if (m.isError) {
|
|
117
|
+
const note = errorResultMediaNote({ images: sendableImages(Array.isArray(rest) ? rest : []).length, documents: docs.length });
|
|
118
|
+
if (note !== undefined) {
|
|
119
|
+
if (typeof inner === "string")
|
|
120
|
+
inner = inner ? `${inner}\n${note}` : note;
|
|
121
|
+
else
|
|
122
|
+
inner.push({ type: "text", text: note });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
115
125
|
if (typeof inner === "string" ? inner.length === 0 : inner.length === 0)
|
|
116
126
|
inner = NO_CONTENT;
|
|
117
127
|
raw.push({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Model } from "../internal/llm.js";
|
|
2
|
+
export declare function modelSupportsVision(model?: Model): boolean;
|
|
3
|
+
export declare const IMAGE_OMITTED_NO_VISION = "[image omitted: the serving model does not support image input]";
|
|
4
|
+
export declare function imagesOmittedNoVisionNote(count: number): string;
|
|
5
|
+
export interface SendableImage {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function sendableImages(blocks: readonly unknown[]): SendableImage[];
|
|
11
|
+
export declare function errorResultMediaNote(counts: {
|
|
12
|
+
images: number;
|
|
13
|
+
documents: number;
|
|
14
|
+
}): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function modelSupportsVision(model) {
|
|
2
|
+
return model?.input === undefined ? true : model.input.includes("image");
|
|
3
|
+
}
|
|
4
|
+
export const IMAGE_OMITTED_NO_VISION = "[image omitted: the serving model does not support image input]";
|
|
5
|
+
export function imagesOmittedNoVisionNote(count) {
|
|
6
|
+
return count > 1 ? `[${count} images omitted: the serving model does not support image input]` : IMAGE_OMITTED_NO_VISION;
|
|
7
|
+
}
|
|
8
|
+
export function sendableImages(blocks) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const b of blocks) {
|
|
11
|
+
const c = b;
|
|
12
|
+
if (c.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string") {
|
|
13
|
+
out.push({ type: "image", data: c.data, mimeType: c.mimeType });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function plural(count, noun) {
|
|
19
|
+
return `${count} ${noun}${count > 1 ? "s" : ""}`;
|
|
20
|
+
}
|
|
21
|
+
export function errorResultMediaNote(counts) {
|
|
22
|
+
const dropped = [];
|
|
23
|
+
if (counts.images > 0)
|
|
24
|
+
dropped.push(plural(counts.images, "image"));
|
|
25
|
+
if (counts.documents > 0)
|
|
26
|
+
dropped.push(plural(counts.documents, "document"));
|
|
27
|
+
if (dropped.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
return `[${dropped.join(" and ")} omitted — an error tool result must be text-only; re-run the tool successfully if you need the attachment]`;
|
|
30
|
+
}
|
package/dist/brain/openai.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
7
7
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
8
|
+
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
9
|
import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
9
10
|
import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
@@ -30,10 +31,6 @@ function textOf(content) {
|
|
|
30
31
|
.map((c) => c.text)
|
|
31
32
|
.join("");
|
|
32
33
|
}
|
|
33
|
-
function visionOf(model) {
|
|
34
|
-
return model?.input === undefined ? true : model.input.includes("image");
|
|
35
|
-
}
|
|
36
|
-
const IMAGE_OMITTED = "[image omitted: the serving model does not support image input]";
|
|
37
34
|
const NO_CONTENT = "(no content)";
|
|
38
35
|
const DOCUMENT_OMITTED = "[PDF document omitted: PDF direct read requires an Anthropic-family model; re-Read the file (the engine will extract text/render pages when possible) or extract the text via Bash instead (e.g. `pdftotext`).]";
|
|
39
36
|
function toUserContent(content, supportsVision = true) {
|
|
@@ -54,7 +51,7 @@ function toUserContent(content, supportsVision = true) {
|
|
|
54
51
|
const n = content.filter((c) => c.type === "image").length;
|
|
55
52
|
emitBrainTelemetry({ kind: "vision_placeholder", count: n });
|
|
56
53
|
const base = textOf(content);
|
|
57
|
-
const note = n
|
|
54
|
+
const note = imagesOmittedNoVisionNote(n);
|
|
58
55
|
const withNote = base ? `${base}\n${note}` : note;
|
|
59
56
|
return nDocs > 0 ? `${withNote}\n${DOCUMENT_OMITTED}` : withNote;
|
|
60
57
|
}
|
|
@@ -139,7 +136,7 @@ function applyThinking(body, model, reasoning) {
|
|
|
139
136
|
}
|
|
140
137
|
}
|
|
141
138
|
function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
142
|
-
const supportsVision =
|
|
139
|
+
const supportsVision = modelSupportsVision(model);
|
|
143
140
|
const out = [];
|
|
144
141
|
const needsEmptyReasoningField = !!model?.reasoning && thinkingCompat(model).requiresReasoningContentOnAssistantMessages === true;
|
|
145
142
|
if (ctx.systemPrompt) {
|
|
@@ -194,9 +191,8 @@ function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
|
194
191
|
out.push(msg);
|
|
195
192
|
}
|
|
196
193
|
else if (m.role === "toolResult") {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
: [];
|
|
194
|
+
const mediaImages = sendableImages(Array.isArray(m.content) ? m.content : []);
|
|
195
|
+
const rawImages = m.isError ? [] : mediaImages;
|
|
200
196
|
const images = supportsVision ? rawImages : [];
|
|
201
197
|
if (!supportsVision && rawImages.length > 0)
|
|
202
198
|
emitBrainTelemetry({ kind: "vision_placeholder", count: rawImages.length });
|
|
@@ -205,11 +201,13 @@ function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
|
205
201
|
emitBrainTelemetry({ kind: "document_placeholder", count: nDocs });
|
|
206
202
|
const baseText0 = textOf(m.content);
|
|
207
203
|
const baseText = nDocs > 0 ? (baseText0 ? `${baseText0}\n${DOCUMENT_OMITTED}` : DOCUMENT_OMITTED) : baseText0;
|
|
208
|
-
const text = !supportsVision && rawImages.length > 0 ? (baseText ? `${baseText}\n${
|
|
204
|
+
const text = !supportsVision && rawImages.length > 0 ? (baseText ? `${baseText}\n${IMAGE_OMITTED_NO_VISION}` : IMAGE_OMITTED_NO_VISION) : baseText;
|
|
205
|
+
const errorNote = m.isError ? errorResultMediaNote({ images: mediaImages.length, documents: 0 }) : undefined;
|
|
206
|
+
const finalText = errorNote === undefined ? text : text ? `${text}\n${errorNote}` : errorNote;
|
|
209
207
|
out.push({
|
|
210
208
|
role: "tool",
|
|
211
209
|
tool_call_id: m.toolCallId,
|
|
212
|
-
content: toolResultText(
|
|
210
|
+
content: toolResultText(finalText, images.length > 0, m.content),
|
|
213
211
|
});
|
|
214
212
|
for (const img of images)
|
|
215
213
|
pendingImages.push({ toolCallId: m.toolCallId, data: img.data, mimeType: img.mimeType });
|
package/dist/brain/timeout.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StreamFn } from "../internal/llm.js";
|
|
1
2
|
export interface BrainTimeoutConfig {
|
|
2
3
|
connectTimeoutMs?: number;
|
|
3
4
|
firstTokenTimeoutMs?: number;
|
|
@@ -10,3 +11,16 @@ export declare function createConnectController(connectTimeoutMs: number | undef
|
|
|
10
11
|
clearTimer: () => void;
|
|
11
12
|
dispose: () => void;
|
|
12
13
|
};
|
|
14
|
+
export declare const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3600000;
|
|
15
|
+
export declare const BRAIN_CALL_GUARDRAIL_ERROR_CODE = "brain.call_guardrail_timeout";
|
|
16
|
+
export declare class BrainCallGuardrailTimeoutError extends Error {
|
|
17
|
+
readonly code = "brain.call_guardrail_timeout";
|
|
18
|
+
readonly limitMs: number;
|
|
19
|
+
constructor(limitMs: number);
|
|
20
|
+
}
|
|
21
|
+
export type BrainCallGuardrailKnob = number | false;
|
|
22
|
+
export declare function resolveBrainCallGuardrailMs(knob: BrainCallGuardrailKnob | undefined): number | undefined;
|
|
23
|
+
export interface BrainCallGuardrailRef {
|
|
24
|
+
timedOut?: BrainCallGuardrailTimeoutError;
|
|
25
|
+
}
|
|
26
|
+
export declare function withBrainCallGuardrail(streamFn: StreamFn, limitMs: number, ref: BrainCallGuardrailRef): StreamFn;
|
package/dist/brain/timeout.js
CHANGED
|
@@ -44,3 +44,117 @@ export function createConnectController(connectTimeoutMs, outerSignal, deadlineA
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
export const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3_600_000;
|
|
48
|
+
export const BRAIN_CALL_GUARDRAIL_ERROR_CODE = "brain.call_guardrail_timeout";
|
|
49
|
+
export class BrainCallGuardrailTimeoutError extends Error {
|
|
50
|
+
code = BRAIN_CALL_GUARDRAIL_ERROR_CODE;
|
|
51
|
+
limitMs;
|
|
52
|
+
constructor(limitMs) {
|
|
53
|
+
super(`the engine stopped waiting for a brain call that ran past its ${limitMs}ms guardrail — the call was abandoned (a Brain that ignores options.signal may still be running)`);
|
|
54
|
+
this.name = "BrainCallGuardrailTimeoutError";
|
|
55
|
+
this.limitMs = limitMs;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function resolveBrainCallGuardrailMs(knob) {
|
|
59
|
+
if (knob === undefined)
|
|
60
|
+
return BRAIN_CALL_GUARDRAIL_DEFAULT_MS;
|
|
61
|
+
if (knob === false || knob === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (typeof knob !== "number" || !Number.isFinite(knob) || knob < 0) {
|
|
64
|
+
const e = new Error(`brainCallGuardrailMs must be a positive number of milliseconds, 0, or false (got ${String(knob)})`);
|
|
65
|
+
e.code = "config.brain_call_guardrail_invalid";
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
return knob;
|
|
69
|
+
}
|
|
70
|
+
function armCallGuardrail(limitMs, outerSignal, ref) {
|
|
71
|
+
const ac = new AbortController();
|
|
72
|
+
if (outerSignal !== undefined) {
|
|
73
|
+
if (outerSignal.aborted)
|
|
74
|
+
ac.abort(outerSignal.reason);
|
|
75
|
+
}
|
|
76
|
+
const forwardAbort = () => {
|
|
77
|
+
ac.abort(outerSignal === undefined ? undefined : outerSignal.reason);
|
|
78
|
+
};
|
|
79
|
+
if (outerSignal !== undefined && !outerSignal.aborted) {
|
|
80
|
+
outerSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
81
|
+
}
|
|
82
|
+
let expired;
|
|
83
|
+
let rejectExpiry = () => undefined;
|
|
84
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
85
|
+
rejectExpiry = reject;
|
|
86
|
+
});
|
|
87
|
+
expiry.catch(() => undefined);
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
const err = new BrainCallGuardrailTimeoutError(limitMs);
|
|
90
|
+
expired = err;
|
|
91
|
+
ref.timedOut ??= err;
|
|
92
|
+
ac.abort(err);
|
|
93
|
+
rejectExpiry(err);
|
|
94
|
+
}, limitMs);
|
|
95
|
+
return {
|
|
96
|
+
signal: ac.signal,
|
|
97
|
+
race: async (p) => {
|
|
98
|
+
if (expired !== undefined)
|
|
99
|
+
throw expired;
|
|
100
|
+
const value = await Promise.race([p, expiry]);
|
|
101
|
+
if (expired !== undefined)
|
|
102
|
+
throw expired;
|
|
103
|
+
return value;
|
|
104
|
+
},
|
|
105
|
+
disarm: () => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
if (outerSignal !== undefined)
|
|
108
|
+
outerSignal.removeEventListener("abort", forwardAbort);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function withBrainCallGuardrail(streamFn, limitMs, ref) {
|
|
113
|
+
return async (model, context, options) => {
|
|
114
|
+
const gate = armCallGuardrail(limitMs, options?.signal, ref);
|
|
115
|
+
let inner;
|
|
116
|
+
try {
|
|
117
|
+
inner = await gate.race(Promise.resolve(streamFn(model, context, { ...options, signal: gate.signal })));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
gate.disarm();
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
[Symbol.asyncIterator]() {
|
|
125
|
+
const it = inner[Symbol.asyncIterator]();
|
|
126
|
+
return {
|
|
127
|
+
next: async () => {
|
|
128
|
+
let step;
|
|
129
|
+
try {
|
|
130
|
+
step = await gate.race(Promise.resolve(it.next()));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
gate.disarm();
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (step.done === true)
|
|
137
|
+
gate.disarm();
|
|
138
|
+
return step;
|
|
139
|
+
},
|
|
140
|
+
return: async () => {
|
|
141
|
+
gate.disarm();
|
|
142
|
+
const release = it.return;
|
|
143
|
+
if (release !== undefined) {
|
|
144
|
+
Promise.resolve(release.call(it)).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
return { value: undefined, done: true };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
result: async () => {
|
|
151
|
+
try {
|
|
152
|
+
return await gate.race(Promise.resolve(inner.result()));
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
gate.disarm();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -30,4 +30,3 @@ export declare function sliceStreamIncrement(s: StreamCursorState, terminal: boo
|
|
|
30
30
|
droppedBeforeCursor: number;
|
|
31
31
|
};
|
|
32
32
|
export declare function markTruncated(text: string, droppedBytes: number): string;
|
|
33
|
-
export declare function markLiveSpoolRotations(text: string, liveRotations: number): string;
|
|
@@ -73,9 +73,3 @@ export function markTruncated(text, droppedBytes) {
|
|
|
73
73
|
? `[... ${droppedBytes} earlier byte(s) truncated; showing last ${Buffer.byteLength(text, "utf8")} ...]\n${text}`
|
|
74
74
|
: text;
|
|
75
75
|
}
|
|
76
|
-
export function markLiveSpoolRotations(text, liveRotations) {
|
|
77
|
-
if (liveRotations <= 0)
|
|
78
|
-
return text;
|
|
79
|
-
const note = `[spool reclaimed ${liveRotations} time(s) while the process was still writing; bytes appended during those windows may be missing beyond any counted truncation]`;
|
|
80
|
-
return text === "" ? note : `${text}${text.endsWith("\n") ? "" : "\n"}${note}`;
|
|
81
|
-
}
|
|
@@ -11,6 +11,7 @@ import { type ActiveSkillFrame } from "./active-skill-scope.js";
|
|
|
11
11
|
import { type CutKillRegistry } from "./cut-kill.js";
|
|
12
12
|
import type { SessionPermissionRules } from "../session-policy-store.js";
|
|
13
13
|
import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
|
|
14
|
+
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
14
15
|
import { type OutputRef, type BlockedRef, type SkillListingEntry } from "./synthetic-tools.js";
|
|
15
16
|
import type { MemoryEngine } from "../memory-engine/engine.js";
|
|
16
17
|
import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
|
|
@@ -136,6 +137,7 @@ export interface Prepared {
|
|
|
136
137
|
callIssuedAtRef?: {
|
|
137
138
|
current?: number;
|
|
138
139
|
};
|
|
140
|
+
brainCallGuardrailRef: BrainCallGuardrailRef;
|
|
139
141
|
reviewRequestRef: {
|
|
140
142
|
pending?: {
|
|
141
143
|
reason?: string;
|
|
@@ -29,6 +29,7 @@ import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js
|
|
|
29
29
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
30
30
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
31
31
|
import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
|
|
32
|
+
import { resolveBrainCallGuardrailMs, withBrainCallGuardrail } from "../../brain/timeout.js";
|
|
32
33
|
import { defineTool, isDefineToolProduct } from "../tools.js";
|
|
33
34
|
import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
|
|
34
35
|
import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
|
|
@@ -236,6 +237,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
236
237
|
e.code = "config.empty_objective";
|
|
237
238
|
throw e;
|
|
238
239
|
}
|
|
240
|
+
const brainCallGuardrailRef = {};
|
|
241
|
+
const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
|
|
239
242
|
if (spec.agents !== undefined && spec.agents.length > 0) {
|
|
240
243
|
const pool = spec.tools ?? [];
|
|
241
244
|
if (!pool.some((t) => typeof t.withAgents === "function")) {
|
|
@@ -2198,7 +2201,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2198
2201
|
systemPrompt,
|
|
2199
2202
|
...(assembled.systemBlocks ? { systemBlocks: assembled.systemBlocks } : {}),
|
|
2200
2203
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
2201
|
-
runtime: brainToRuntime(
|
|
2204
|
+
runtime: brainToRuntime(brainCallGuardrailMs === undefined
|
|
2205
|
+
? deps.brain
|
|
2206
|
+
: {
|
|
2207
|
+
...deps.brain,
|
|
2208
|
+
stream: withBrainCallGuardrail((m, c, o) => deps.brain.stream(m, c, o), brainCallGuardrailMs, brainCallGuardrailRef),
|
|
2209
|
+
}),
|
|
2202
2210
|
});
|
|
2203
2211
|
harnessRef.current = harness;
|
|
2204
2212
|
let releaseSignal = () => undefined;
|
|
@@ -3529,7 +3537,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3529
3537
|
: undefined;
|
|
3530
3538
|
overheadState.promptChars = systemPrompt.length;
|
|
3531
3539
|
const preparedHolder = {};
|
|
3532
|
-
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, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), 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 } : {}) });
|
|
3540
|
+
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, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), 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 } : {}) });
|
|
3533
3541
|
const prepared = buildPrepared();
|
|
3534
3542
|
preparedHolder.current = prepared;
|
|
3535
3543
|
return prepared;
|
|
@@ -2659,6 +2659,9 @@ export class Runner {
|
|
|
2659
2659
|
unsubBoundary();
|
|
2660
2660
|
unsub();
|
|
2661
2661
|
}
|
|
2662
|
+
if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
|
|
2663
|
+
threw = prepared.brainCallGuardrailRef.timedOut;
|
|
2664
|
+
}
|
|
2662
2665
|
const suspended = prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined;
|
|
2663
2666
|
const reviewPaused = prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined;
|
|
2664
2667
|
const durablyPaused = suspended || reviewPaused;
|
|
@@ -1016,7 +1016,7 @@ export function buildAgentPollDetails(input) {
|
|
|
1016
1016
|
status: input.status,
|
|
1017
1017
|
retrieval_status: input.retrievalStatus,
|
|
1018
1018
|
...(input.seq !== undefined ? { seq: input.seq } : {}),
|
|
1019
|
-
...(input.
|
|
1019
|
+
...(input.stoppedBy !== undefined ? { stoppedBy: input.stoppedBy } : {}),
|
|
1020
1020
|
...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
|
|
1021
1021
|
...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
|
|
1022
1022
|
...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
|
package/dist/core/types.d.ts
CHANGED
|
@@ -333,6 +333,7 @@ export interface TaskSpec {
|
|
|
333
333
|
};
|
|
334
334
|
callCapByDeadline?: false;
|
|
335
335
|
gracefulFinalize?: false;
|
|
336
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
336
337
|
};
|
|
337
338
|
configOverrides?: ReadonlyArray<import("../config/catalog.js").ConfigOverrideDeclaration>;
|
|
338
339
|
forwardSubagentEvents?: boolean;
|
|
@@ -754,6 +755,7 @@ export interface RunnerDeps {
|
|
|
754
755
|
builtinWorkflows?: boolean;
|
|
755
756
|
onWorkflowAgentSpawn?: (handle: import("../orchestration/workflow.js").WorkflowAgentHandle) => void;
|
|
756
757
|
maxSuspends?: number;
|
|
758
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
757
759
|
now?: () => number;
|
|
758
760
|
memoryBackend?: import("./memory-engine/types.js").MemoryBackend;
|
|
759
761
|
memoryEngineDir?: string;
|
|
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline";
|
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
10
|
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
|
-
import { RollingTailBuffer,
|
|
11
|
+
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
12
12
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
13
13
|
import { SchedulerError } from "../../core/scheduler.js";
|
|
14
14
|
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
|
@@ -216,18 +216,14 @@ const EXEC_SPOOL_HARD_CAP_BYTES = 16 * 1024 * 1024;
|
|
|
216
216
|
export function decideSpoolReclaim(args) {
|
|
217
217
|
const rotate = args.rotateBytes ?? EXEC_SPOOL_ROTATE_BYTES;
|
|
218
218
|
const hardCap = args.hardCapBytes ?? EXEC_SPOOL_HARD_CAP_BYTES;
|
|
219
|
-
if (args.
|
|
219
|
+
if (args.writerLive)
|
|
220
220
|
return { reclaim: false };
|
|
221
|
-
if (args.
|
|
222
|
-
if (!args.writerLive)
|
|
223
|
-
return { reclaim: true, live: false, skippedBytes: 0 };
|
|
224
|
-
if (args.size >= hardCap)
|
|
225
|
-
return { reclaim: true, live: true, skippedBytes: 0 };
|
|
221
|
+
if (args.cursor < rotate)
|
|
226
222
|
return { reclaim: false };
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
if (args.size === args.cursor)
|
|
224
|
+
return { reclaim: true, skippedBytes: 0 };
|
|
225
|
+
if (args.size > args.cursor && args.size >= hardCap)
|
|
226
|
+
return { reclaim: true, skippedBytes: args.size - args.cursor };
|
|
231
227
|
return { reclaim: false };
|
|
232
228
|
}
|
|
233
229
|
const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
@@ -330,8 +326,6 @@ export class NodeExecutionEnv {
|
|
|
330
326
|
const adoptOnTimeoutRef = {};
|
|
331
327
|
const pumpSpoolRef = {};
|
|
332
328
|
const drainSpoolRef = {};
|
|
333
|
-
const liveRotations = { out: 0, err: 0 };
|
|
334
|
-
const liveRotationsTotal = () => liveRotations.out + liveRotations.err;
|
|
335
329
|
const writerLive = () => !settled && child !== undefined && child.exitCode === null && child.signalCode === null;
|
|
336
330
|
const armForceSettle = (result) => {
|
|
337
331
|
if (forceSettleRef.current || settled)
|
|
@@ -351,7 +345,7 @@ export class NodeExecutionEnv {
|
|
|
351
345
|
drainSpoolRef.current?.();
|
|
352
346
|
const so = stdoutTail.result();
|
|
353
347
|
const se = stderrTail.result();
|
|
354
|
-
return { stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
348
|
+
return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
|
|
355
349
|
};
|
|
356
350
|
const onAbort = () => {
|
|
357
351
|
abortKillInFlight = true;
|
|
@@ -545,8 +539,6 @@ export class NodeExecutionEnv {
|
|
|
545
539
|
return;
|
|
546
540
|
if (d.skippedBytes > 0)
|
|
547
541
|
lane.tail.recordSkippedBytes(d.skippedBytes);
|
|
548
|
-
if (d.live)
|
|
549
|
-
liveRotations[lane.cursor]++;
|
|
550
542
|
truncateSync(lane.path, 0);
|
|
551
543
|
spoolCursor[lane.cursor] = 0;
|
|
552
544
|
}
|
|
@@ -583,9 +575,7 @@ export class NodeExecutionEnv {
|
|
|
583
575
|
if (settled || !child)
|
|
584
576
|
return false;
|
|
585
577
|
pumpSpoolRef.current?.();
|
|
586
|
-
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined
|
|
587
|
-
? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err }, liveRotations: liveRotationsTotal() }
|
|
588
|
-
: undefined);
|
|
578
|
+
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
|
|
589
579
|
if (shellId === undefined)
|
|
590
580
|
return false;
|
|
591
581
|
if (spoolPumpRef.current)
|
|
@@ -678,12 +668,12 @@ export class NodeExecutionEnv {
|
|
|
678
668
|
const signum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
679
669
|
settle(ok({
|
|
680
670
|
stdout: markTruncated(so.text, so.droppedBytes),
|
|
681
|
-
stderr:
|
|
671
|
+
stderr: `${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`,
|
|
682
672
|
exitCode: 128 + (signum[signal] ?? 9),
|
|
683
673
|
}));
|
|
684
674
|
return;
|
|
685
675
|
}
|
|
686
|
-
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
676
|
+
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes), exitCode: code ?? 0 }));
|
|
687
677
|
};
|
|
688
678
|
child.on("close", (code, signal) => settleFromTermination(code, signal));
|
|
689
679
|
child.on("exit", (code, signal) => {
|
|
@@ -1051,7 +1041,6 @@ export class NodeExecutionEnv {
|
|
|
1051
1041
|
...(handover !== undefined
|
|
1052
1042
|
? { spool: { outPath: handover.spool.outPath, errPath: handover.spool.errPath, fileCursor: { ...handover.cursors }, ephemeral: true } }
|
|
1053
1043
|
: {}),
|
|
1054
|
-
...(handover !== undefined && (handover.liveRotations ?? 0) > 0 ? { liveRotations: handover.liveRotations } : {}),
|
|
1055
1044
|
};
|
|
1056
1045
|
const seedOut = Buffer.from(stdoutSoFar.text, "utf8");
|
|
1057
1046
|
const seedErr = Buffer.from(stderrSoFar.text, "utf8");
|
|
@@ -1241,14 +1230,9 @@ export class NodeExecutionEnv {
|
|
|
1241
1230
|
const out = sliceStreamIncrement(entry.stdout, terminal);
|
|
1242
1231
|
const er = sliceStreamIncrement(entry.stderr, terminal);
|
|
1243
1232
|
const droppedBeforeCursor = out.droppedBeforeCursor + er.droppedBeforeCursor;
|
|
1244
|
-
let stderrInc = er.inc;
|
|
1245
|
-
if (terminal && (entry.liveRotations ?? 0) > 0 && entry.liveRotationsDisclosed !== true) {
|
|
1246
|
-
entry.liveRotationsDisclosed = true;
|
|
1247
|
-
stderrInc = markLiveSpoolRotations(er.inc, entry.liveRotations ?? 0);
|
|
1248
|
-
}
|
|
1249
1233
|
return ok({
|
|
1250
1234
|
stdout: out.inc,
|
|
1251
|
-
stderr:
|
|
1235
|
+
stderr: er.inc,
|
|
1252
1236
|
status: entry.status,
|
|
1253
1237
|
...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
|
|
1254
1238
|
...(droppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: droppedBeforeCursor } : {}),
|
|
@@ -1320,8 +1304,6 @@ export class NodeExecutionEnv {
|
|
|
1320
1304
|
stream.tail.recordSkippedBytes(d.skippedBytes);
|
|
1321
1305
|
stream.totalBytes += d.skippedBytes;
|
|
1322
1306
|
}
|
|
1323
|
-
if (d.live)
|
|
1324
|
-
entry.liveRotations = (entry.liveRotations ?? 0) + 1;
|
|
1325
1307
|
truncateSync(path, 0);
|
|
1326
1308
|
spool.fileCursor[lane] = 0;
|
|
1327
1309
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export { toolResultStoreContract } from "./core/store-contracts/tool-result-stor
|
|
|
165
165
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
166
166
|
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
167
167
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
168
|
-
export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, type BackgroundAgentQuery, } from "./core/background-agent-store.js";
|
|
168
|
+
export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, type BackgroundAgentQuery, } from "./core/background-agent-store.js";
|
|
169
169
|
export { serveDurableAgentRowLane, buildAgentPollDetails, type AgentPollDetailsInput, } from "./core/task-registry-agent.js";
|
|
170
170
|
export type { UnifiedTaskResult } from "./core/task-registry-shared.js";
|
|
171
171
|
export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "./stores/file/background-agent-store.js";
|
package/dist/index.js
CHANGED
|
@@ -151,7 +151,7 @@ export { toolResultStoreContract } from "./core/store-contracts/tool-result-stor
|
|
|
151
151
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
152
152
|
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
153
153
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
154
|
-
export { canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
154
|
+
export { canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
155
155
|
export { serveDurableAgentRowLane, buildAgentPollDetails, } from "./core/task-registry-agent.js";
|
|
156
156
|
export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.js";
|
|
157
157
|
export { A2A_TASK_STATES, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-task-state.js";
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -5,7 +5,7 @@ import { decodeTextBytes } from "./encoding.js";
|
|
|
5
5
|
import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
|
|
6
6
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
|
|
7
7
|
import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
|
|
8
|
-
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
8
|
+
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
9
9
|
import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
|
|
10
10
|
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption) {
|
|
11
11
|
return defineTool({
|
|
@@ -70,14 +70,16 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
70
70
|
}
|
|
71
71
|
const downsampler = imageDownsampler === false ? undefined : (imageDownsampler ?? (await resolveAutoDownsampler()));
|
|
72
72
|
const readCap = downsampler ? MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES : MAX_IMAGE_READ_BYTES;
|
|
73
|
+
const tooLargeToRead = (bytes) => `Error (Read): image "${path}" is too large to read (${bytes} bytes > ${readCap}-byte cap).` +
|
|
74
|
+
(downsampler ? "" : ` ${NO_DOWNSAMPLER_IMAGE_CAP_HINT}`);
|
|
73
75
|
if (meta.value.size > readCap) {
|
|
74
|
-
return errorResult(
|
|
76
|
+
return errorResult(tooLargeToRead(meta.value.size));
|
|
75
77
|
}
|
|
76
78
|
const bin = await env.readBinaryFile(r.key, ctx.signal);
|
|
77
79
|
if (!bin.ok)
|
|
78
80
|
return errorResult(`Error (Read): cannot read image "${path}": ${bin.error.message}`);
|
|
79
81
|
if (bin.value.byteLength > readCap) {
|
|
80
|
-
return errorResult(
|
|
82
|
+
return errorResult(tooLargeToRead(bin.value.byteLength));
|
|
81
83
|
}
|
|
82
84
|
if (!imageMagicMatches(bin.value, imageMime)) {
|
|
83
85
|
return errorResult(`Error (Read): "${path}" has an image extension but its content is not a valid ${imageMime} (empty, truncated, or mis-named).`);
|
|
@@ -18,6 +18,7 @@ export declare function persistedTextOf(encoded: string | Uint8Array): string;
|
|
|
18
18
|
export declare function notReadRefusalText(env: ExecutionEnv, toolName: string, key: string, v: Pick<FsViolation, "code" | "message" | "partialView">, signal?: AbortSignal, fallbackHint?: string): Promise<string>;
|
|
19
19
|
export declare const MAX_IMAGE_READ_BYTES: number;
|
|
20
20
|
export declare const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES: number;
|
|
21
|
+
export declare const NO_DOWNSAMPLER_IMAGE_CAP_HINT = "Install the optional \"sharp\" dependency (declared in this package's optionalDependencies) to read images up to 67108864 bytes \u2014 it downsamples them to fit the API image limit.";
|
|
21
22
|
export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
|
|
22
23
|
export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | undefined>;
|
|
23
24
|
export declare const MAX_READ_OUTPUT_CHARS = 100000;
|
|
@@ -41,6 +41,7 @@ export async function notReadRefusalText(env, toolName, key, v, signal, fallback
|
|
|
41
41
|
}
|
|
42
42
|
export const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024;
|
|
43
43
|
export const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES = 64 * 1024 * 1024;
|
|
44
|
+
export const NO_DOWNSAMPLER_IMAGE_CAP_HINT = `Install the optional "sharp" dependency (declared in this package's optionalDependencies) to read images up to ${MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES} bytes — it downsamples them to fit the API image limit.`;
|
|
44
45
|
let autoDownsampler;
|
|
45
46
|
export function resolveAutoDownsampler() {
|
|
46
47
|
return (autoDownsampler ??= sharpImageDownsampler());
|
package/dist/tools/web.js
CHANGED
|
@@ -211,11 +211,14 @@ async function fetchAllowlisted(doFetch, start, allowHosts, userAgent, signal, o
|
|
|
211
211
|
export function webFetchToolSpec(config = {}) {
|
|
212
212
|
const maxBytes = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
213
213
|
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
214
|
+
const canSummarize = config.summarize !== undefined;
|
|
214
215
|
return {
|
|
215
216
|
name: "WebFetch",
|
|
216
217
|
contract: { contractId: "core.web_fetch@1", implementationRevision: "1" },
|
|
217
|
-
description:
|
|
218
|
-
"
|
|
218
|
+
description: (canSummarize
|
|
219
|
+
? "Fetches a URL, converts the page to markdown, and answers `prompt` against it with a sub-model.\n"
|
|
220
|
+
: "Fetches a URL, converts the page to markdown, and returns the markdown. This deployment has no " +
|
|
221
|
+
"extraction sub-model wired, so pages come back in full for you to read.\n") +
|
|
219
222
|
"\n" +
|
|
220
223
|
"- Fails on authenticated/private URLs — use an authenticated MCP tool or `gh` for those instead.\n" +
|
|
221
224
|
"- Cross-host redirects are returned to you rather than followed; call again with the redirect URL.\n" +
|
|
@@ -223,7 +226,9 @@ export function webFetchToolSpec(config = {}) {
|
|
|
223
226
|
"hosts are always refused; if an allowlist is configured, only those hosts are reachable.",
|
|
224
227
|
parameters: Type.Object({
|
|
225
228
|
url: Type.String({ description: "The URL to fetch content from" }),
|
|
226
|
-
|
|
229
|
+
...(canSummarize
|
|
230
|
+
? { prompt: Type.Optional(Type.String({ description: "The prompt to run on the fetched content (answered by the configured sub-model)." })) }
|
|
231
|
+
: {}),
|
|
227
232
|
}),
|
|
228
233
|
effect: "read",
|
|
229
234
|
execute: async (args, ctx) => {
|
|
@@ -434,9 +439,12 @@ export function webFetchToolSpec(config = {}) {
|
|
|
434
439
|
: bodyCut
|
|
435
440
|
? `\n\n[WebFetch: ${cutPhrase} — the error body itself was cut off mid-transfer: ${bodyBytes?.length ?? 0} bytes were received before the cutoff and the tail is missing]`
|
|
436
441
|
: "";
|
|
442
|
+
const promptNote = prompt
|
|
443
|
+
? `\n\n[note: the requested analysis ("${inlineUntrusted(prompt, 120)}") was NOT applied — the fetch failed with HTTP ${res.status}, so there is no page content to run it against; anything below is the server's error response, not an answer]`
|
|
444
|
+
: "";
|
|
437
445
|
const content = (excerpt
|
|
438
446
|
? `${headline}\nThe error response body${trimmed.length > excerpt.length ? ` (first ${ERROR_BODY_EXCERPT_CHARS} chars)` : ""} follows:\n\n${delimitUntrusted(`WebFetch ${parsed.hostname}`, excerpt)}`
|
|
439
|
-
: headline) + bodyStateNote;
|
|
447
|
+
: headline) + bodyStateNote + promptNote;
|
|
440
448
|
return {
|
|
441
449
|
content,
|
|
442
450
|
details: {
|