@sema-agent/core 3.0.0 → 4.0.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 +2345 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/cascade.js +31 -32
- package/dist/agents/cumulative-stats.d.ts +29 -0
- package/dist/agents/cumulative-stats.js +29 -0
- package/dist/agents/observer.d.ts +4 -0
- package/dist/agents/observer.js +11 -10
- package/dist/agents/repair-loop.js +27 -19
- package/dist/agents/retain-ledger.d.ts +2 -0
- package/dist/agents/retain-ledger.js +6 -5
- package/dist/agents/roster-store.js +3 -5
- package/dist/agents/subagent.js +66 -64
- package/dist/agents/teacher.d.ts +1 -9
- package/dist/agents/teacher.js +21 -24
- package/dist/agents/verify.js +15 -14
- package/dist/brain/anthropic.js +24 -21
- package/dist/brain/openai.js +24 -21
- package/dist/brain/stream-engine.js +5 -2
- package/dist/brain/stream-shared.d.ts +1 -0
- package/dist/brain/stream-shared.js +14 -0
- package/dist/brain/terminal-cause.d.ts +4 -0
- package/dist/brain/terminal-cause.js +12 -0
- package/dist/brain/tool-call-id.d.ts +1 -0
- package/dist/brain/tool-call-id.js +3 -0
- package/dist/brain/tool-call-repair.js +2 -1
- package/dist/core/auto-compaction.js +16 -25
- package/dist/core/background-agent-store.d.ts +22 -22
- package/dist/core/background-agent-store.js +19 -23
- package/dist/core/checkpoint-store.d.ts +6 -13
- package/dist/core/checkpoint-store.js +11 -5
- package/dist/core/file-snapshot-store.d.ts +8 -0
- package/dist/core/file-snapshot-store.js +4 -3
- package/dist/core/hooks.d.ts +3 -1
- package/dist/core/hooks.js +8 -18
- package/dist/core/lsp.js +4 -3
- package/dist/core/mailbox-store.d.ts +7 -10
- package/dist/core/mcp.d.ts +7 -10
- package/dist/core/mcp.js +7 -3
- package/dist/core/memory-engine/engine.d.ts +0 -1
- package/dist/core/memory-engine/engine.js +24 -34
- package/dist/core/memory-engine/file-backend.d.ts +1 -2
- package/dist/core/memory-engine/file-backend.js +25 -36
- package/dist/core/memory-engine/layout.d.ts +8 -0
- package/dist/core/memory-engine/layout.js +60 -4
- package/dist/core/memory-engine/scope-contract.js +9 -3
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/runner/active-skill-scope.d.ts +0 -1
- package/dist/core/runner/active-skill-scope.js +1 -15
- package/dist/core/runner/assemble-result.d.ts +2 -7
- package/dist/core/runner/assemble-result.js +3 -4
- package/dist/core/runner/compaction-call-options.d.ts +7 -0
- package/dist/core/runner/compaction-call-options.js +30 -0
- package/dist/core/runner/prepare-task.d.ts +6 -23
- package/dist/core/runner/prepare-task.js +20 -53
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +65 -156
- package/dist/core/runner/session-rule-policy.js +4 -1
- package/dist/core/runner/teardown-bounded.d.ts +7 -0
- package/dist/core/runner/teardown-bounded.js +36 -0
- package/dist/core/runner/tool-disclosure.d.ts +2 -5
- package/dist/core/runner/turn-attachments.d.ts +5 -15
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/safe-notify.d.ts +17 -0
- package/dist/core/safe-notify.js +59 -0
- package/dist/core/sensitive-path-policy.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/stub-env.d.ts +2 -6
- package/dist/core/surrogate-safe-slice.d.ts +4 -0
- package/dist/core/surrogate-safe-slice.js +18 -0
- package/dist/core/task-registry-monitor.js +8 -13
- package/dist/core/task-registry-shared.d.ts +22 -0
- package/dist/core/task-registry-shared.js +16 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +40 -57
- package/dist/core/tool-errors.js +5 -4
- package/dist/core/tool-name-aliases.js +2 -1
- package/dist/core/tool-policy.js +10 -5
- package/dist/core/tool-result-store.d.ts +1 -0
- package/dist/core/tool-result-store.js +17 -1
- package/dist/core/trace.d.ts +2 -9
- package/dist/core/types.d.ts +15 -12
- package/dist/core/untrusted-egress.js +7 -5
- package/dist/core/workflow-journal-store.d.ts +9 -20
- package/dist/core/workflow-run-store.d.ts +11 -10
- package/dist/core/workflow-run-store.js +22 -16
- package/dist/engine/compaction/utils.js +4 -2
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +13 -33
- package/dist/engine/harness/types.d.ts +29 -67
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +11 -4
- package/dist/engine/loop/types.d.ts +11 -9
- package/dist/engine/session/memory-repo.d.ts +2 -6
- package/dist/engine/session/session.d.ts +5 -30
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +3 -1
- package/dist/orchestration/run-spec.js +7 -4
- package/dist/orchestration/run-workflow-tool.js +3 -35
- package/dist/orchestration/workflow.d.ts +3 -11
- package/dist/orchestration/workflow.js +164 -204
- package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
- package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
- package/dist/stores/file/background-agent-store.d.ts +3 -12
- package/dist/stores/file/background-agent-store.js +3 -41
- package/dist/stores/file/checkpoint-store.js +6 -6
- package/dist/stores/file/mailbox-store.d.ts +2 -6
- package/dist/stores/file/session-policy-store.js +15 -1
- package/dist/stores/file/session-store.d.ts +2 -6
- package/dist/stores/file/workflow-journal-store.d.ts +3 -11
- package/dist/stores/file/workflow-run-store.d.ts +3 -7
- package/dist/stores/file/workflow-run-store.js +4 -17
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +60 -25
- package/dist/tools/fs/fs-bash.js +324 -96
- package/dist/tools/fs/fs-pdf.d.ts +2 -12
- package/dist/tools/fs/fs-shared.js +2 -2
- package/dist/tools/fs/notebook.d.ts +2 -5
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +43 -14
- package/dist/tools/fs/search.js +5 -5
- package/dist/tools/monitor.d.ts +1 -0
- package/dist/tools/monitor.js +7 -5
- package/dist/tools/worktree.d.ts +2 -7
- package/dist/tools/worktree.js +3 -1
- package/package.json +6 -2
package/dist/brain/openai.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {} from "../internal/llm.js";
|
|
2
2
|
import { repairTextToolCalls } from "./tool-call-repair.js";
|
|
3
3
|
import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
|
|
4
|
-
import { createRepetitionPoll, createWalltimeGate } from "./stream-shared.js";
|
|
4
|
+
import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from "./stream-shared.js";
|
|
5
|
+
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
5
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
6
7
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
8
|
import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
@@ -12,23 +13,11 @@ function closeToolCallAccum(acc) {
|
|
|
12
13
|
return acc.closedTc;
|
|
13
14
|
if (!acc.name)
|
|
14
15
|
return undefined;
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
const parsed = JSON.parse(acc.args);
|
|
19
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
20
|
-
return undefined;
|
|
21
|
-
args = parsed;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
args = {};
|
|
29
|
-
}
|
|
16
|
+
const args = parseStreamedToolArgs(acc.args);
|
|
17
|
+
if (args === undefined)
|
|
18
|
+
return undefined;
|
|
30
19
|
if (!acc.id)
|
|
31
|
-
acc.id =
|
|
20
|
+
acc.id = mintFallbackToolCallId("call_");
|
|
32
21
|
acc.closedTc = { type: "toolCall", id: acc.id, name: acc.name, arguments: args };
|
|
33
22
|
return acc.closedTc;
|
|
34
23
|
}
|
|
@@ -356,6 +345,8 @@ export function createOpenAIBrain(config = {}) {
|
|
|
356
345
|
const toolAccum = new Map();
|
|
357
346
|
let finishReason = null;
|
|
358
347
|
let usageRaw;
|
|
348
|
+
let malformedFrames = 0;
|
|
349
|
+
let malformedSample;
|
|
359
350
|
const thinkingBlock = { type: "thinking", thinking: "" };
|
|
360
351
|
const textBlock = { type: "text", text: "" };
|
|
361
352
|
const pushReasoningDelta = (delta) => {
|
|
@@ -438,6 +429,8 @@ export function createOpenAIBrain(config = {}) {
|
|
|
438
429
|
chunk = JSON.parse(data);
|
|
439
430
|
}
|
|
440
431
|
catch {
|
|
432
|
+
malformedFrames++;
|
|
433
|
+
malformedSample ??= data.slice(0, 160);
|
|
441
434
|
return;
|
|
442
435
|
}
|
|
443
436
|
if (chunk.usage)
|
|
@@ -535,6 +528,12 @@ export function createOpenAIBrain(config = {}) {
|
|
|
535
528
|
text: `\n[note: ${malformed.length} tool call(s) were truncated (finish_reason="${finishReason ?? "?"}") and dropped — re-issue them next turn: ${malformed.join("; ")}]`,
|
|
536
529
|
});
|
|
537
530
|
}
|
|
531
|
+
if (malformedFrames > 0) {
|
|
532
|
+
finalContent.push({
|
|
533
|
+
type: "text",
|
|
534
|
+
text: `\n[note: ${malformedFrames} provider stream frame(s) were not valid JSON and were dropped — the streamed output may be missing content (first sample: ${JSON.stringify(malformedSample)})]`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
538
537
|
const truncatedEmpty = finishReason === "length" && noUsableContent && !toolError;
|
|
539
538
|
const dynamicCut = truncatedEmpty && options?.maxTokensDynamic === true;
|
|
540
539
|
const safetyCut = finishReason === "content_filter";
|
|
@@ -557,7 +556,7 @@ export function createOpenAIBrain(config = {}) {
|
|
|
557
556
|
? `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning (${accumReasoning.length} reasoning chars). Raise max_tokens, or disable thinking for this task.`
|
|
558
557
|
: `[length_empty] response truncated at max_tokens (finish_reason="length", ${maxTokensNote}) with no answer text — raise max_tokens`
|
|
559
558
|
: emptyNoFinish
|
|
560
|
-
? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)`
|
|
559
|
+
? `[stream_torn] model stream ended with no content and no finish_reason — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
|
|
561
560
|
: undefined));
|
|
562
561
|
const errored = walltime.cut ||
|
|
563
562
|
degenerate ||
|
|
@@ -566,9 +565,13 @@ export function createOpenAIBrain(config = {}) {
|
|
|
566
565
|
emptyNoFinish ||
|
|
567
566
|
(toolError !== undefined && noUsableContent) ||
|
|
568
567
|
(truncatedEmpty && !dynamicCut);
|
|
569
|
-
const errorKind =
|
|
570
|
-
? "
|
|
571
|
-
:
|
|
568
|
+
const errorKind = walltime.cut
|
|
569
|
+
? "walltime_cutoff"
|
|
570
|
+
: degenerate
|
|
571
|
+
? "degenerate"
|
|
572
|
+
: !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
|
|
573
|
+
? "length_empty"
|
|
574
|
+
: undefined;
|
|
572
575
|
const finalMessage = {
|
|
573
576
|
...partial,
|
|
574
577
|
content: finalContent.length > 0 ? finalContent : [{ type: "text", text: "" }],
|
|
@@ -8,7 +8,7 @@ const DEFAULT_MAX_RETRIES = 10;
|
|
|
8
8
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
9
9
|
const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
|
|
10
10
|
export function resolveMaxRetries(configured) {
|
|
11
|
-
if (configured !== undefined)
|
|
11
|
+
if (configured !== undefined && Number.isFinite(configured))
|
|
12
12
|
return configured;
|
|
13
13
|
const raw = process.env[MAX_RETRIES_ENV];
|
|
14
14
|
if (raw !== undefined && raw.trim() !== "") {
|
|
@@ -88,10 +88,13 @@ export function runStreamingBrain(args) {
|
|
|
88
88
|
.catch((err) => {
|
|
89
89
|
const aborted = signal?.aborted === true || isAbortError(err);
|
|
90
90
|
terminalRetryPhase = "gave_up";
|
|
91
|
-
|
|
91
|
+
const walltimeCut = isWalltimeCutoff(err);
|
|
92
|
+
terminalRetryDetail = aborted ? "cancelled while retrying" : walltimeCut ? "wall-clock deadline reached while retrying" : "retries exhausted";
|
|
92
93
|
const errorMsg = emptyAssistant(model);
|
|
93
94
|
errorMsg.stopReason = aborted ? "aborted" : "error";
|
|
94
95
|
errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
|
|
96
|
+
if (walltimeCut)
|
|
97
|
+
errorMsg.errorKind = "walltime_cutoff";
|
|
95
98
|
errorMsg.usageMissing = true;
|
|
96
99
|
out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
|
|
97
100
|
})
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { inspectDegenerate } from "./repetition.js";
|
|
2
|
+
export function parseStreamedToolArgs(json) {
|
|
3
|
+
if (!json)
|
|
4
|
+
return {};
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(json);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
13
|
+
return undefined;
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
2
16
|
export function createRepetitionPoll() {
|
|
3
17
|
let repCut;
|
|
4
18
|
const repSpared = new Map();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AssistantMessage } from "../internal/llm.js";
|
|
2
|
+
export type TerminalCauseCarrier = Pick<AssistantMessage, "errorKind" | "errorMessage">;
|
|
3
|
+
export declare function isDegenerateCutMessage(message: TerminalCauseCarrier): boolean;
|
|
4
|
+
export declare function isWalltimeCutMessage(message: TerminalCauseCarrier): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DEGENERATE_MESSAGE } from "./repetition.js";
|
|
2
|
+
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
3
|
+
export function isDegenerateCutMessage(message) {
|
|
4
|
+
if (message.errorKind === "degenerate")
|
|
5
|
+
return true;
|
|
6
|
+
return message.errorMessage === DEGENERATE_MESSAGE;
|
|
7
|
+
}
|
|
8
|
+
export function isWalltimeCutMessage(message) {
|
|
9
|
+
if (message.errorKind === "walltime_cutoff")
|
|
10
|
+
return true;
|
|
11
|
+
return message.errorMessage === WALLTIME_CUTOFF_MESSAGE;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mintFallbackToolCallId(prefix: string): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
1
2
|
const TAG_RE = /<(tool_call|function_call|tool_calls|function_calls)>\s*([\s\S]*?)\s*<\/\1>/gi;
|
|
2
3
|
const FENCE_RE = /```(?:json|tool_call|tool|function)?\s*([\s\S]*?)```/gi;
|
|
3
4
|
function asRecord(v) {
|
|
@@ -41,7 +42,7 @@ function extractCall(obj, toolNames, framed) {
|
|
|
41
42
|
}
|
|
42
43
|
return {
|
|
43
44
|
type: "toolCall",
|
|
44
|
-
id: `repair_${name}_
|
|
45
|
+
id: mintFallbackToolCallId(`repair_${name}_`),
|
|
45
46
|
name,
|
|
46
47
|
arguments: args,
|
|
47
48
|
};
|
|
@@ -5,6 +5,7 @@ import { selectCompactionEpoch } from "../prompt-assembly/epoch.js";
|
|
|
5
5
|
import { SEMA_DEFAULT_PACK } from "../prompt-assembly/packs/sema-default.js";
|
|
6
6
|
import { brainToRuntime } from "./runtime.js";
|
|
7
7
|
import { defuseFenceMarkers, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
8
|
+
import { createSafeNotifier } from "./safe-notify.js";
|
|
8
9
|
export const DEFAULT_COMPACTION_INSTRUCTIONS = "Preserve verbatim every code, identifier, name, number, file path, error message, credential " +
|
|
9
10
|
"reference, and explicit user instruction or decision mentioned anywhere in the conversation. " +
|
|
10
11
|
"When in doubt, keep the specific detail rather than generalizing it.";
|
|
@@ -96,6 +97,7 @@ export async function maybeCompact(opts) {
|
|
|
96
97
|
return { contextUsage, compacted: false, noop: true };
|
|
97
98
|
}
|
|
98
99
|
const trigger = opts.trigger ?? "auto";
|
|
100
|
+
const notifier = createSafeNotifier();
|
|
99
101
|
let hookInstructions;
|
|
100
102
|
if (opts.preCompact) {
|
|
101
103
|
let pre;
|
|
@@ -183,11 +185,7 @@ export async function maybeCompact(opts) {
|
|
|
183
185
|
}
|
|
184
186
|
const contentTokens = dry !== undefined ? Math.ceil((dry.conversationChars + (prep.value.previousSummary?.length ?? 0)) / cpt) : 0;
|
|
185
187
|
const observeWindowSafety = (info) => {
|
|
186
|
-
|
|
187
|
-
opts.onWindowSafety?.(info);
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
}
|
|
188
|
+
notifier.notify(() => opts.onWindowSafety?.(info), "compaction.onWindowSafety");
|
|
191
189
|
};
|
|
192
190
|
if (wantsWindowSafety && dry !== undefined) {
|
|
193
191
|
const tolerance = settings.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
|
|
@@ -498,27 +496,20 @@ export async function maybeCompact(opts) {
|
|
|
498
496
|
}
|
|
499
497
|
catch {
|
|
500
498
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
}
|
|
499
|
+
const messagesCompacted = prep.value.messagesToSummarize.length;
|
|
500
|
+
notifier.notify(() => opts.onCompaction?.({
|
|
501
|
+
messagesCompacted,
|
|
502
|
+
freedTokens,
|
|
503
|
+
tokensBefore: summaryTokensBefore,
|
|
504
|
+
reused,
|
|
505
|
+
}), "compaction.onCompaction");
|
|
511
506
|
if (opts.postCompact) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
}
|
|
507
|
+
await notifier.notifyAsync(() => opts.postCompact?.({
|
|
508
|
+
trigger,
|
|
509
|
+
summary,
|
|
510
|
+
tokensBefore: summaryTokensBefore,
|
|
511
|
+
...(postTriggerTokens !== undefined ? { tokensAfter: postTriggerTokens } : {}),
|
|
512
|
+
}), "compaction.postCompact");
|
|
522
513
|
}
|
|
523
514
|
const endAt = Date.now();
|
|
524
515
|
return {
|
|
@@ -73,18 +73,24 @@ export declare function canAccessAgentRecord(record: Pick<BackgroundAgentRecord,
|
|
|
73
73
|
sessionId?: string;
|
|
74
74
|
}): boolean;
|
|
75
75
|
export declare const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running \u2014 its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped).";
|
|
76
|
+
export interface BackgroundAgentUpdateIfExpect {
|
|
77
|
+
rev: number;
|
|
78
|
+
status?: BackgroundAgentRecord["status"];
|
|
79
|
+
parkedCheckpointToken?: string | null;
|
|
80
|
+
parkClaimId?: string | null;
|
|
81
|
+
}
|
|
82
|
+
export interface BackgroundAgentReapOptions {
|
|
83
|
+
maxAgeMs?: number;
|
|
84
|
+
keep?: number;
|
|
85
|
+
staleRunningMaxAgeMs?: number;
|
|
86
|
+
}
|
|
76
87
|
export interface BackgroundAgentStore {
|
|
77
88
|
put(record: BackgroundAgentRecord): Promise<void>;
|
|
78
89
|
get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>;
|
|
79
90
|
update(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
|
|
80
91
|
rev: number;
|
|
81
92
|
}): Promise<boolean>;
|
|
82
|
-
updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect:
|
|
83
|
-
rev: number;
|
|
84
|
-
status?: BackgroundAgentRecord["status"];
|
|
85
|
-
parkedCheckpointToken?: string | null;
|
|
86
|
-
parkClaimId?: string | null;
|
|
87
|
-
}): Promise<boolean>;
|
|
93
|
+
updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>;
|
|
88
94
|
listBySession(scope: string, sessionId: string, opts?: {
|
|
89
95
|
status?: BackgroundAgentRecord["status"];
|
|
90
96
|
limit?: number;
|
|
@@ -97,11 +103,7 @@ export interface BackgroundAgentStore {
|
|
|
97
103
|
rev: number;
|
|
98
104
|
}): Promise<boolean>;
|
|
99
105
|
listScopes?(): Promise<string[]>;
|
|
100
|
-
reap(scope: string, now: number, opts?:
|
|
101
|
-
maxAgeMs?: number;
|
|
102
|
-
keep?: number;
|
|
103
|
-
staleRunningMaxAgeMs?: number;
|
|
104
|
-
}): Promise<number>;
|
|
106
|
+
reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>;
|
|
105
107
|
}
|
|
106
108
|
export declare function reconcileParkedAgents(stores: {
|
|
107
109
|
agentStore: BackgroundAgentStore;
|
|
@@ -114,6 +116,13 @@ export declare function reconcileParkedAgents(stores: {
|
|
|
114
116
|
failed: number;
|
|
115
117
|
rolledBack: number;
|
|
116
118
|
}>;
|
|
119
|
+
export interface BackgroundAgentQuery {
|
|
120
|
+
scope: string;
|
|
121
|
+
sessionId?: string;
|
|
122
|
+
status?: BackgroundAgentRecord["status"];
|
|
123
|
+
limit?: number;
|
|
124
|
+
}
|
|
125
|
+
export declare function queryBackgroundAgents(rows: Iterable<BackgroundAgentRecord>, query: BackgroundAgentQuery): BackgroundAgentRowSummary[];
|
|
117
126
|
export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStore {
|
|
118
127
|
private rows;
|
|
119
128
|
private key;
|
|
@@ -122,12 +131,7 @@ export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStor
|
|
|
122
131
|
update(handle: string, scope: string, record: BackgroundAgentRecord, expect: {
|
|
123
132
|
rev: number;
|
|
124
133
|
}): Promise<boolean>;
|
|
125
|
-
updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect:
|
|
126
|
-
rev: number;
|
|
127
|
-
status?: BackgroundAgentRecord["status"];
|
|
128
|
-
parkedCheckpointToken?: string | null;
|
|
129
|
-
parkClaimId?: string | null;
|
|
130
|
-
}): Promise<boolean>;
|
|
134
|
+
updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>;
|
|
131
135
|
listBySession(scope: string, sessionId: string, opts?: {
|
|
132
136
|
status?: BackgroundAgentRecord["status"];
|
|
133
137
|
limit?: number;
|
|
@@ -140,9 +144,5 @@ export declare class InMemoryBackgroundAgentStore implements BackgroundAgentStor
|
|
|
140
144
|
rev: number;
|
|
141
145
|
}): Promise<boolean>;
|
|
142
146
|
listScopes(): Promise<string[]>;
|
|
143
|
-
reap(scope: string, now: number, opts?:
|
|
144
|
-
maxAgeMs?: number;
|
|
145
|
-
keep?: number;
|
|
146
|
-
staleRunningMaxAgeMs?: number;
|
|
147
|
-
}): Promise<number>;
|
|
147
|
+
reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>;
|
|
148
148
|
}
|
|
@@ -173,6 +173,23 @@ function toSummary(r) {
|
|
|
173
173
|
...(r.usage !== undefined ? { usage: structuredClone(r.usage) } : {}),
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
+
export function queryBackgroundAgents(rows, query) {
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const r of rows) {
|
|
179
|
+
if (r.scope !== query.scope)
|
|
180
|
+
continue;
|
|
181
|
+
if (query.sessionId !== undefined) {
|
|
182
|
+
const anchor = r.sessionScoped ? r.owner : r.parentSessionId;
|
|
183
|
+
if (anchor !== query.sessionId && r.rootSessionId !== query.sessionId)
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (query.status !== undefined && r.status !== query.status)
|
|
187
|
+
continue;
|
|
188
|
+
out.push(toSummary(r));
|
|
189
|
+
}
|
|
190
|
+
out.sort((a, b) => b.spawnedAt - a.spawnedAt);
|
|
191
|
+
return query.limit !== undefined ? out.slice(0, Math.max(0, query.limit)) : out;
|
|
192
|
+
}
|
|
176
193
|
export class InMemoryBackgroundAgentStore {
|
|
177
194
|
rows = new Map();
|
|
178
195
|
key(handle, scope) {
|
|
@@ -220,31 +237,10 @@ export class InMemoryBackgroundAgentStore {
|
|
|
220
237
|
return this.update(handle, scope, record, { rev: expect.rev });
|
|
221
238
|
}
|
|
222
239
|
async listBySession(scope, sessionId, opts) {
|
|
223
|
-
|
|
224
|
-
for (const r of this.rows.values()) {
|
|
225
|
-
if (r.scope !== scope)
|
|
226
|
-
continue;
|
|
227
|
-
const anchor = r.sessionScoped ? r.owner : r.parentSessionId;
|
|
228
|
-
if (anchor !== sessionId && r.rootSessionId !== sessionId)
|
|
229
|
-
continue;
|
|
230
|
-
if (opts?.status !== undefined && r.status !== opts.status)
|
|
231
|
-
continue;
|
|
232
|
-
out.push(toSummary(r));
|
|
233
|
-
}
|
|
234
|
-
out.sort((a, b) => b.spawnedAt - a.spawnedAt);
|
|
235
|
-
return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
|
|
240
|
+
return queryBackgroundAgents(this.rows.values(), { scope, sessionId, status: opts?.status, limit: opts?.limit });
|
|
236
241
|
}
|
|
237
242
|
async listByScope(scope, opts) {
|
|
238
|
-
|
|
239
|
-
for (const r of this.rows.values()) {
|
|
240
|
-
if (r.scope !== scope)
|
|
241
|
-
continue;
|
|
242
|
-
if (opts?.status !== undefined && r.status !== opts.status)
|
|
243
|
-
continue;
|
|
244
|
-
out.push(toSummary(r));
|
|
245
|
-
}
|
|
246
|
-
out.sort((a, b) => b.spawnedAt - a.spawnedAt);
|
|
247
|
-
return opts?.limit !== undefined ? out.slice(0, Math.max(0, opts.limit)) : out;
|
|
243
|
+
return queryBackgroundAgents(this.rows.values(), { scope, status: opts?.status, limit: opts?.limit });
|
|
248
244
|
}
|
|
249
245
|
async delete(handle, scope, expect) {
|
|
250
246
|
const k = this.key(handle, scope);
|
|
@@ -3,7 +3,8 @@ import type { RepairBundle } from "../agents/repair-loop.js";
|
|
|
3
3
|
import type { WorkspaceHandle } from "./remote-env.js";
|
|
4
4
|
import type { ConsolidationNote } from "./runner/memory-consolidation.js";
|
|
5
5
|
import type { SessionPermissionRules } from "./session-policy-store.js";
|
|
6
|
-
import type { TaskResult } from "./types.js";
|
|
6
|
+
import type { NestedUsageAccum, TaskResult } from "./types.js";
|
|
7
|
+
import type { ActiveWorktreeSession } from "../internal/harness-types.js";
|
|
7
8
|
export type CheckpointToken = string & {
|
|
8
9
|
readonly __brand: "CheckpointToken";
|
|
9
10
|
};
|
|
@@ -112,12 +113,7 @@ export interface CheckpointState {
|
|
|
112
113
|
value?: unknown;
|
|
113
114
|
set?: boolean;
|
|
114
115
|
};
|
|
115
|
-
nestedStats:
|
|
116
|
-
tokens: number;
|
|
117
|
-
turns: number;
|
|
118
|
-
tasks: number;
|
|
119
|
-
costMicroUsd: number;
|
|
120
|
-
};
|
|
116
|
+
nestedStats: NestedUsageAccum;
|
|
121
117
|
consolidationNotes?: ConsolidationNote[];
|
|
122
118
|
readFileState?: Array<[string, ReadEntry]>;
|
|
123
119
|
repairBundle?: RepairBundle;
|
|
@@ -131,12 +127,7 @@ export interface CheckpointState {
|
|
|
131
127
|
description?: string;
|
|
132
128
|
}>;
|
|
133
129
|
handsCwd?: string;
|
|
134
|
-
activeWorktree?:
|
|
135
|
-
worktreeDir: string;
|
|
136
|
-
originalCwd: string;
|
|
137
|
-
baseSha: string;
|
|
138
|
-
entered?: boolean;
|
|
139
|
-
};
|
|
130
|
+
activeWorktree?: ActiveWorktreeSession;
|
|
140
131
|
inheritedGate?: {
|
|
141
132
|
ancestorRules?: ReadonlyArray<{
|
|
142
133
|
sessionId: string;
|
|
@@ -273,6 +264,8 @@ export declare function validatePendingSteer(steer: {
|
|
|
273
264
|
text: string;
|
|
274
265
|
trusted: boolean;
|
|
275
266
|
};
|
|
267
|
+
export declare function checkpointRowMatches(cp: Checkpoint | undefined, scope: string, status: Checkpoint["status"]): cp is Checkpoint;
|
|
268
|
+
export declare function checkpointOccMatches(cp: Checkpoint, expect?: ResolveExpectation): boolean;
|
|
276
269
|
export type CheckpointFaultMode = "resolve-after-commit" | "resolve-before-commit";
|
|
277
270
|
export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
278
271
|
private cps;
|
|
@@ -237,6 +237,12 @@ export function validatePendingSteer(steer) {
|
|
|
237
237
|
}
|
|
238
238
|
return { text: steer.text, trusted: steer.trusted };
|
|
239
239
|
}
|
|
240
|
+
export function checkpointRowMatches(cp, scope, status) {
|
|
241
|
+
return cp !== undefined && cp.scope === scope && cp.status === status;
|
|
242
|
+
}
|
|
243
|
+
export function checkpointOccMatches(cp, expect) {
|
|
244
|
+
return expect === undefined || (cp.rev ?? 0) === expect.rev;
|
|
245
|
+
}
|
|
240
246
|
export class InMemoryCheckpointStore {
|
|
241
247
|
cps = new Map();
|
|
242
248
|
fault = null;
|
|
@@ -256,10 +262,10 @@ export class InMemoryCheckpointStore {
|
|
|
256
262
|
throw new Error("injected fault: resolve before commit");
|
|
257
263
|
}
|
|
258
264
|
const cp = this.cps.get(token);
|
|
259
|
-
if (!cp
|
|
265
|
+
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
260
266
|
return false;
|
|
261
267
|
}
|
|
262
|
-
if (
|
|
268
|
+
if (!checkpointOccMatches(cp, expect)) {
|
|
263
269
|
return false;
|
|
264
270
|
}
|
|
265
271
|
cp.status = "resolved";
|
|
@@ -276,7 +282,7 @@ export class InMemoryCheckpointStore {
|
|
|
276
282
|
}
|
|
277
283
|
async reopen(token, scope, reason) {
|
|
278
284
|
const cp = this.cps.get(token);
|
|
279
|
-
if (!cp
|
|
285
|
+
if (!checkpointRowMatches(cp, scope, "resolved")) {
|
|
280
286
|
return false;
|
|
281
287
|
}
|
|
282
288
|
cp.status = "pending";
|
|
@@ -287,7 +293,7 @@ export class InMemoryCheckpointStore {
|
|
|
287
293
|
async setPendingSteer(token, scope, steer) {
|
|
288
294
|
const clean = validatePendingSteer(steer);
|
|
289
295
|
const cp = this.cps.get(token);
|
|
290
|
-
if (!cp
|
|
296
|
+
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
291
297
|
return false;
|
|
292
298
|
}
|
|
293
299
|
cp.state.pendingSteer = clean;
|
|
@@ -295,7 +301,7 @@ export class InMemoryCheckpointStore {
|
|
|
295
301
|
}
|
|
296
302
|
async expire(token, scope) {
|
|
297
303
|
const cp = this.cps.get(token);
|
|
298
|
-
if (!cp
|
|
304
|
+
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
299
305
|
return false;
|
|
300
306
|
}
|
|
301
307
|
cp.status = "expired";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare function relUnder(root: string, child: string): string | null;
|
|
2
|
+
declare function joinUnder(root: string, rel: string): string;
|
|
1
3
|
import type { ExecutionEnv } from "../internal/harness-types.js";
|
|
2
4
|
export interface FileSnapshotError {
|
|
3
5
|
code: "too_large" | "enumerate_failed" | "read_failed" | "restore_failed" | "not_found" | "aborted";
|
|
@@ -51,3 +53,9 @@ export declare class InMemoryFileSnapshotStore implements FileSnapshotStore {
|
|
|
51
53
|
importManifest(scope: string, key: string, manifest: Map<string, string>, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileSnapshotResult>;
|
|
52
54
|
reap(scope: string, keepKeys: string[]): Promise<number>;
|
|
53
55
|
}
|
|
56
|
+
export declare const __testables: {
|
|
57
|
+
joinUnder: typeof joinUnder;
|
|
58
|
+
relUnder: typeof relUnder;
|
|
59
|
+
stripTrailSep: (p: string) => string;
|
|
60
|
+
};
|
|
61
|
+
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolutePathForm, isWinFormPath } from "../tools/fs/safety.js";
|
|
2
3
|
const normSlash = (p) => p.replace(/\\/g, "/");
|
|
3
|
-
const stripTrailSep = (p) => p.replace(/[\\/]+$/, "");
|
|
4
|
+
const stripTrailSep = (p) => (isWinFormPath(p) ? p.replace(/[\\/]+$/, "") : p.replace(/\/+$/, ""));
|
|
4
5
|
const foldDrive = (p) => (/^[A-Za-z]:(\/|$)/.test(p) ? p[0].toLowerCase() + p.slice(1) : p);
|
|
5
|
-
const isWinFormPath = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
|
|
6
6
|
function relUnder(root, child) {
|
|
7
7
|
const fold = isWinFormPath(root) ? normSlash : (p) => p;
|
|
8
8
|
const r = foldDrive(fold(stripTrailSep(root)));
|
|
@@ -18,7 +18,7 @@ function joinUnder(root, rel) {
|
|
|
18
18
|
return stripTrailSep(root) + (win ? "\\" : "/") + (win ? rel.replace(/\//g, "\\") : rel);
|
|
19
19
|
}
|
|
20
20
|
function isUnsafeRelPath(relN) {
|
|
21
|
-
return relN
|
|
21
|
+
return isAbsolutePathForm(relN) || /^[A-Za-z]:/.test(relN) || relN.split("/").includes("..");
|
|
22
22
|
}
|
|
23
23
|
export const DEFAULT_SNAPSHOT_BOUNDS = {
|
|
24
24
|
maxFiles: 10000,
|
|
@@ -256,3 +256,4 @@ export class InMemoryFileSnapshotStore {
|
|
|
256
256
|
return removed;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
export const __testables = { joinUnder, relUnder, stripTrailSep };
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export interface Hooks {
|
|
|
7
7
|
userPromptSubmit?(prompt: string): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
|
|
8
8
|
stop?(ctx: StopHookContext): StopHookResult | undefined | Promise<StopHookResult | undefined>;
|
|
9
9
|
postToolUseFailure?(toolName: string, input: unknown, failure: HookToolFailure, ctx: HookToolContext): PostToolUseFailureResult | undefined | Promise<PostToolUseFailureResult | undefined>;
|
|
10
|
-
postToolBatch?(batch: PostToolBatchCall[]
|
|
10
|
+
postToolBatch?(batch: PostToolBatchCall[], meta?: {
|
|
11
|
+
injectedThisTurn: "final_verification" | "finalize";
|
|
12
|
+
}): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
|
|
11
13
|
preCompact?(ctx: PreCompactContext): PreCompactResult | undefined | Promise<PreCompactResult | undefined>;
|
|
12
14
|
postCompact?(ctx: PostCompactContext): void | Promise<void>;
|
|
13
15
|
stopFailure?(ctx: StopFailureContext): void | Promise<void>;
|
package/dist/core/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decisionText } from "./tool-policy.js";
|
|
2
2
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
3
3
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
4
|
+
import { createSafeNotifier } from "./safe-notify.js";
|
|
4
5
|
export function cloneObserverInput(input) {
|
|
5
6
|
try {
|
|
6
7
|
return structuredClone(input);
|
|
@@ -23,12 +24,8 @@ function preToolUseCrashReason(subject, err) {
|
|
|
23
24
|
`This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the ` +
|
|
24
25
|
`same hook and fails the same way. Hook error: ${cause || "(no message)"}`);
|
|
25
26
|
}
|
|
26
|
-
function traceHookCrash(input, err) {
|
|
27
|
-
|
|
28
|
-
input.onHookError?.(err);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
}
|
|
27
|
+
function traceHookCrash(input, err, notifier) {
|
|
28
|
+
notifier.notify(() => input.onHookError?.(err), "toolGate.onHookError");
|
|
32
29
|
}
|
|
33
30
|
function withProbeTimeout(p, ms, signal) {
|
|
34
31
|
if (ms === undefined && signal === undefined)
|
|
@@ -62,6 +59,7 @@ export async function runToolGate(input) {
|
|
|
62
59
|
let currentInput = event.input;
|
|
63
60
|
const preToolContext = [];
|
|
64
61
|
let hookAsk;
|
|
62
|
+
const notifier = createSafeNotifier();
|
|
65
63
|
if (preToolUse) {
|
|
66
64
|
let r;
|
|
67
65
|
try {
|
|
@@ -69,13 +67,9 @@ export async function runToolGate(input) {
|
|
|
69
67
|
}
|
|
70
68
|
catch (err) {
|
|
71
69
|
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
72
|
-
traceHookCrash(input, err);
|
|
70
|
+
traceHookCrash(input, err, notifier);
|
|
73
71
|
if (input.permissionDenied) {
|
|
74
|
-
|
|
75
|
-
await input.permissionDenied({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" });
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
}
|
|
72
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" }), "toolGate.permissionDenied");
|
|
79
73
|
}
|
|
80
74
|
return { block: true, reason: formatHookFeedback(reason), preToolContext };
|
|
81
75
|
}
|
|
@@ -216,7 +210,7 @@ export async function runToolGate(input) {
|
|
|
216
210
|
hr = await preToolUse(toolName, editArgs, { toolCallId, toolName });
|
|
217
211
|
}
|
|
218
212
|
catch (err) {
|
|
219
|
-
traceHookCrash(input, err);
|
|
213
|
+
traceHookCrash(input, err, notifier);
|
|
220
214
|
editDenied = { action: "deny", reason: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
|
|
221
215
|
denySource = "hook";
|
|
222
216
|
break;
|
|
@@ -288,11 +282,7 @@ export async function runToolGate(input) {
|
|
|
288
282
|
currentInput = decision.updatedInput;
|
|
289
283
|
}
|
|
290
284
|
if (input.permissionDenied) {
|
|
291
|
-
|
|
292
|
-
await input.permissionDenied({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource });
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
}
|
|
285
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource }), "toolGate.permissionDenied");
|
|
296
286
|
}
|
|
297
287
|
return {
|
|
298
288
|
block: true,
|
package/dist/core/lsp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { uriToPath } from "./lsp-protocol.js";
|
|
2
|
+
import { foldRedundantPathSegments, isAbsolutePathForm } from "../tools/fs/safety.js";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
import { defineTool, errorResult } from "./tools.js";
|
|
4
5
|
export const LSP_OPERATIONS = [
|
|
@@ -67,10 +68,10 @@ export function settleOnAbort(job, signal, onAbort) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
export function resolveLspPath(filePath, cwd) {
|
|
70
|
-
const isAbsForm =
|
|
71
|
+
const isAbsForm = isAbsolutePathForm(filePath);
|
|
71
72
|
if (isAbsForm || !cwd)
|
|
72
|
-
return filePath;
|
|
73
|
-
return cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
|
|
73
|
+
return foldRedundantPathSegments(filePath);
|
|
74
|
+
return foldRedundantPathSegments(cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath);
|
|
74
75
|
}
|
|
75
76
|
export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
|
|
76
77
|
function fmt(result, ctx) {
|