@sema-agent/core 2.12.0 → 2.13.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/dist/agents/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/git-worktree-env.js +5 -0
- package/dist/core/mcp.d.ts +1 -0
- package/dist/core/mcp.js +4 -3
- package/dist/core/runner/active-skill-scope.js +34 -6
- package/dist/core/runner/prepare-task.js +12 -3
- package/dist/core/runner/runtask.js +27 -8
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/types.d.ts +2 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.js +38 -6
- package/dist/tools/scheduler-tools.js +9 -3
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ export interface AgentTranscriptToolOptions {
|
|
|
8
8
|
owner?: string;
|
|
9
9
|
scope?: string;
|
|
10
10
|
sessionId?: string;
|
|
11
|
+
enrichCtx?: import("../core/tools.js").ToolCtxEnricher;
|
|
11
12
|
}
|
|
12
13
|
export declare function createAgentTranscriptTool(opts: AgentTranscriptToolOptions): import("../internal/harness-types.js").AgentTool<Type.TObject<{
|
|
13
14
|
id: Type.TString;
|
|
@@ -43,6 +43,9 @@ function emptyAssistant(model) {
|
|
|
43
43
|
function isAbortError(err) {
|
|
44
44
|
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
45
45
|
}
|
|
46
|
+
function isWalltimeCutoff(err) {
|
|
47
|
+
return err instanceof Error && err.message === WALLTIME_CUTOFF_MESSAGE;
|
|
48
|
+
}
|
|
46
49
|
function sleep(ms, signal) {
|
|
47
50
|
return new Promise((resolve) => {
|
|
48
51
|
if (signal?.aborted)
|
|
@@ -85,7 +88,7 @@ export function runStreamingBrain(args) {
|
|
|
85
88
|
.catch((err) => {
|
|
86
89
|
const aborted = signal?.aborted === true || isAbortError(err);
|
|
87
90
|
terminalRetryPhase = "gave_up";
|
|
88
|
-
terminalRetryDetail = aborted ? "cancelled while retrying" : "retries exhausted";
|
|
91
|
+
terminalRetryDetail = aborted ? "cancelled while retrying" : isWalltimeCutoff(err) ? "wall-clock deadline reached while retrying" : "retries exhausted";
|
|
89
92
|
const errorMsg = emptyAssistant(model);
|
|
90
93
|
errorMsg.stopReason = aborted ? "aborted" : "error";
|
|
91
94
|
errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extractSymbols } from "../tools/fs/repo-map.js";
|
|
2
|
+
import { parseSkillToolEntry } from "./skill-tool-specifier.js";
|
|
2
3
|
export function decideAutoPromote(i) {
|
|
3
4
|
if (i.tripwire.escalate)
|
|
4
5
|
return "needs_human";
|
|
@@ -58,7 +59,7 @@ export function deriveTripwire(artifactText, declared, profileTokens) {
|
|
|
58
59
|
const reasons = [];
|
|
59
60
|
let escalate = false;
|
|
60
61
|
try {
|
|
61
|
-
const allow = new Set(declared.allowTools ?? []);
|
|
62
|
+
const allow = new Set((declared.allowTools ?? []).map((entry) => parseSkillToolEntry(entry).name));
|
|
62
63
|
const symbols = extractSymbols(artifactText);
|
|
63
64
|
for (const sym of symbols) {
|
|
64
65
|
if (!allow.has(sym)) {
|
|
@@ -28,6 +28,11 @@ export async function addWorktree(baseEnv, opts) {
|
|
|
28
28
|
const detail = add.ok ? add.value.stderr || add.value.stdout : String(add.error);
|
|
29
29
|
throw new Error(`git worktree add failed: ${detail}`);
|
|
30
30
|
}
|
|
31
|
+
const excludeLine = shq(`/${WORKTREE_PARENT}/`);
|
|
32
|
+
await baseEnv
|
|
33
|
+
.exec(`ex="$(git rev-parse --git-path info/exclude)" && mkdir -p "$(dirname "$ex")" && ` +
|
|
34
|
+
`{ grep -qxF ${excludeLine} "$ex" 2>/dev/null || printf '%s\\n' ${excludeLine} >> "$ex"; }`, { cwd: opts.repoRoot })
|
|
35
|
+
.catch(() => undefined);
|
|
31
36
|
let inner;
|
|
32
37
|
try {
|
|
33
38
|
inner = await opts.rootEnvAt(worktreeDir);
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
|
|
|
71
71
|
export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
|
|
72
72
|
export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
|
|
73
73
|
export declare function collapseMcpErrorPrefix(message: string): string;
|
|
74
|
+
export declare function networkErrorCode(err: unknown, depth?: number): string | undefined;
|
|
74
75
|
export declare function normalizeMcpName(name: string): string;
|
|
75
76
|
export declare function clampNameSegment(seg: string, max?: number): string;
|
|
76
77
|
export * from "./image-downsample.js";
|
package/dist/core/mcp.js
CHANGED
|
@@ -179,11 +179,11 @@ const NETWORK_CODES_NEVER_DELIVERED = new Set([
|
|
|
179
179
|
"ENETUNREACH",
|
|
180
180
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
181
181
|
]);
|
|
182
|
-
function networkErrorCode(err, depth = 0) {
|
|
182
|
+
export function networkErrorCode(err, depth = 0) {
|
|
183
183
|
if (depth > 5 || !(err instanceof Error))
|
|
184
184
|
return undefined;
|
|
185
185
|
const code = err.code;
|
|
186
|
-
if (typeof code === "string" && /^(?:E[A-
|
|
186
|
+
if (typeof code === "string" && /^(?:E[A-Z_]+|UND_ERR_[A-Z_]+)$/.test(code))
|
|
187
187
|
return code;
|
|
188
188
|
if (err instanceof AggregateError) {
|
|
189
189
|
for (const inner of err.errors) {
|
|
@@ -786,7 +786,8 @@ async function readDirViaExtension(rs, uri, signal, timeoutMs, watchdog) {
|
|
|
786
786
|
if (err instanceof McpError && err.code === ErrorCode.InvalidParams) {
|
|
787
787
|
if (pages > 0)
|
|
788
788
|
return { kind: "ok", resources, cursorInvalid: true };
|
|
789
|
-
|
|
789
|
+
const detail = collapseMcpErrorPrefix(err.message);
|
|
790
|
+
return { kind: classifyDirReadInvalidParams(detail), detail };
|
|
790
791
|
}
|
|
791
792
|
if (err instanceof McpError && err.code === ErrorCode.MethodNotFound)
|
|
792
793
|
return { kind: "unsupported" };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { canonicalToolName } from "../tool-name-aliases.js";
|
|
3
|
+
import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
|
|
3
4
|
export class ActiveSkillScope {
|
|
4
5
|
frames = [];
|
|
5
6
|
push(frame) {
|
|
@@ -47,12 +48,19 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
const manifests = frames.flatMap((f) => (f.kind === "manifest" ? [f.manifest] : []));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const perFrame = manifests.map((m) => {
|
|
52
|
+
const byName = new Map();
|
|
53
|
+
for (const raw of m.allowTools) {
|
|
54
|
+
const entry = parseSkillToolEntry(raw);
|
|
55
|
+
const bucket = byName.get(entry.name);
|
|
56
|
+
if (bucket)
|
|
57
|
+
bucket.push(entry);
|
|
58
|
+
else
|
|
59
|
+
byName.set(entry.name, [entry]);
|
|
60
|
+
}
|
|
61
|
+
return { manifest: m, byName };
|
|
62
|
+
});
|
|
63
|
+
if (perFrame.length === 0 || perFrame.some((f) => !f.byName.has(toolName))) {
|
|
56
64
|
const ids = manifests.map((m) => m.lineageId).join(", ");
|
|
57
65
|
return {
|
|
58
66
|
action: "deny",
|
|
@@ -60,6 +68,26 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
60
68
|
decisionReason: "safety",
|
|
61
69
|
};
|
|
62
70
|
}
|
|
71
|
+
for (const frame of perFrame) {
|
|
72
|
+
const alternatives = frame.byName.get(toolName) ?? [];
|
|
73
|
+
const rejections = [];
|
|
74
|
+
let admitted = false;
|
|
75
|
+
for (const entry of alternatives) {
|
|
76
|
+
const rejection = skillSpecifierRejection(entry, req.args);
|
|
77
|
+
if (rejection === undefined) {
|
|
78
|
+
admitted = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
rejections.push(rejection);
|
|
82
|
+
}
|
|
83
|
+
if (!admitted) {
|
|
84
|
+
return {
|
|
85
|
+
action: "deny",
|
|
86
|
+
reason: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
|
|
87
|
+
decisionReason: "safety",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
63
91
|
const pathConstrainingActive = manifests.some((m) => m.allowPaths && m.allowPaths.length > 0);
|
|
64
92
|
if (pathConstrainingActive && !PATH_WRITE_TOOLS.has(toolName)) {
|
|
65
93
|
const eff = toolEffects?.get(toolName) ?? "write";
|
|
@@ -432,6 +432,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
const taskScope = internals?.registryScope ?? spec.principal ?? "default";
|
|
435
|
+
const offloadScope = spec.principal ?? "default";
|
|
435
436
|
defaultTaskRegistry.maybeGc();
|
|
436
437
|
const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
|
|
437
438
|
const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
|
|
@@ -456,7 +457,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
456
457
|
const offloadEnabled = Number.isFinite(offloadThreshold) && offloadThreshold > 0;
|
|
457
458
|
const rawOffloadStore = offloadEnabled ? (deps.toolResultStore ?? new InMemoryToolResultStore()) : undefined;
|
|
458
459
|
const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
|
|
459
|
-
? new ScopedToolResultStore(rawOffloadStore,
|
|
460
|
+
? new ScopedToolResultStore(rawOffloadStore, offloadScope)
|
|
460
461
|
: rawOffloadStore;
|
|
461
462
|
const offloadReachableToolsRef = {};
|
|
462
463
|
const maybeOffload = (tool, perTool) => {
|
|
@@ -1384,6 +1385,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1384
1385
|
owner: hostTaskId,
|
|
1385
1386
|
scope: taskScope,
|
|
1386
1387
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1388
|
+
enrichCtx: enrichSpecToolCtx,
|
|
1387
1389
|
})));
|
|
1388
1390
|
}
|
|
1389
1391
|
}
|
|
@@ -1915,6 +1917,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1915
1917
|
throw e;
|
|
1916
1918
|
}
|
|
1917
1919
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
1920
|
+
let activationChain = Promise.resolve();
|
|
1921
|
+
const serializeActivation = (section) => {
|
|
1922
|
+
const p = activationChain.then(section);
|
|
1923
|
+
activationChain = p.then(() => undefined, () => undefined);
|
|
1924
|
+
return p;
|
|
1925
|
+
};
|
|
1918
1926
|
const directCallFor = (name) => {
|
|
1919
1927
|
if (spec.deferSelfResolve === false)
|
|
1920
1928
|
return undefined;
|
|
@@ -1930,7 +1938,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1930
1938
|
};
|
|
1931
1939
|
},
|
|
1932
1940
|
...(executionMode !== undefined ? { executionMode } : {}),
|
|
1933
|
-
activate: async () => {
|
|
1941
|
+
activate: async () => serializeActivation(async () => {
|
|
1934
1942
|
if (activeTools.has(name))
|
|
1935
1943
|
return undefined;
|
|
1936
1944
|
activeTools.add(name);
|
|
@@ -1942,7 +1950,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1942
1950
|
throw e;
|
|
1943
1951
|
}
|
|
1944
1952
|
return listingRideRef.current?.([name]);
|
|
1945
|
-
},
|
|
1953
|
+
}),
|
|
1946
1954
|
};
|
|
1947
1955
|
};
|
|
1948
1956
|
const placeholders = new Map([...registry.values()].map((i) => [i.name, createPlaceholderTool(i, directCallFor(i.name))]));
|
|
@@ -2016,6 +2024,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2016
2024
|
listingRide: (newly) => listingRideRef.current?.(newly),
|
|
2017
2025
|
mountedNames: callableToolNames,
|
|
2018
2026
|
directCallEnabled: spec.deferSelfResolve !== false,
|
|
2027
|
+
serializeActivation,
|
|
2019
2028
|
});
|
|
2020
2029
|
harnessTools = buildToolList(activeTools);
|
|
2021
2030
|
}
|
|
@@ -222,6 +222,7 @@ function resumeContinuation(resume) {
|
|
|
222
222
|
}
|
|
223
223
|
function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
224
224
|
const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, turnToolSpan, walltimeDeadlineMs, walltimeMonotonicDeadline, nudgeSchedule, buildFinalizeText, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
|
|
225
|
+
let finalVerifySeenAtLastBoundary = 0;
|
|
225
226
|
const onTurnBoundary = async (event) => {
|
|
226
227
|
if (prepared.callCapRef && turnToolSpan.startMin !== undefined && turnToolSpan.endMax !== undefined) {
|
|
227
228
|
recordToolCycleSample(prepared.callCapRef.state, turnToolSpan.endMax - turnToolSpan.startMin);
|
|
@@ -499,11 +500,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
499
500
|
}
|
|
500
501
|
}
|
|
501
502
|
let batchContextBlock;
|
|
503
|
+
const finalVerifyInjectedThisTurn = rs.counters.finalVerifyInjections !== finalVerifySeenAtLastBoundary;
|
|
504
|
+
finalVerifySeenAtLastBoundary = rs.counters.finalVerifyInjections;
|
|
502
505
|
if (postToolBatchHook !== undefined && rs.turn.toolBatch.length > 0) {
|
|
503
506
|
const batch = rs.turn.toolBatch;
|
|
504
507
|
rs.turn.toolBatch = [];
|
|
505
508
|
batchArgs?.clear();
|
|
506
|
-
if (!boundarySteered && !rs.counters.finalizeInjected &&
|
|
509
|
+
if (!boundarySteered && !rs.counters.finalizeInjected && !finalVerifyInjectedThisTurn && !prepared.abortController.signal.aborted) {
|
|
507
510
|
try {
|
|
508
511
|
const r = await postToolBatchHook(batch);
|
|
509
512
|
if (r?.additionalContext) {
|
|
@@ -1662,6 +1665,9 @@ export class Runner {
|
|
|
1662
1665
|
const gateToolName = "toolName" in resume.cp.gate ? resume.cp.gate.toolName : undefined;
|
|
1663
1666
|
prepared.humanReviewRef.gates.push({ kind: resume.cp.gate.kind, waitMs, ...(decision !== undefined ? { decision } : {}), ...(gateToolName !== undefined ? { toolName: gateToolName } : {}) });
|
|
1664
1667
|
}
|
|
1668
|
+
if (resume !== undefined && resume.outcome.gate === "plan_review" && resume.outcome.decision === "reject") {
|
|
1669
|
+
prepared.planModeRef.active = true;
|
|
1670
|
+
}
|
|
1665
1671
|
const rs = createRunState();
|
|
1666
1672
|
rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
|
|
1667
1673
|
rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
|
|
@@ -2531,13 +2537,26 @@ export class Runner {
|
|
|
2531
2537
|
let promptBlocked = false;
|
|
2532
2538
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
2533
2539
|
if (userPromptSubmit) {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2540
|
+
try {
|
|
2541
|
+
const decision = await userPromptSubmit(spec.objective);
|
|
2542
|
+
if (decision?.block) {
|
|
2543
|
+
prepared.blockedRef.reason = formatHookFeedback(decision.block);
|
|
2544
|
+
promptBlocked = true;
|
|
2545
|
+
}
|
|
2546
|
+
else if (decision?.additionalContext) {
|
|
2547
|
+
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
|
|
2548
|
+
}
|
|
2538
2549
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2550
|
+
catch (hookErr) {
|
|
2551
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
2552
|
+
try {
|
|
2553
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: prepared.sessionId });
|
|
2554
|
+
}
|
|
2555
|
+
catch {
|
|
2556
|
+
}
|
|
2557
|
+
prepared.blockedRef.reason = formatHookFeedback(`the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` +
|
|
2558
|
+
`the prompt was NOT submitted (fail-closed)`);
|
|
2559
|
+
promptBlocked = true;
|
|
2541
2560
|
}
|
|
2542
2561
|
}
|
|
2543
2562
|
const firstFrames = [];
|
|
@@ -3102,7 +3121,7 @@ export class Runner {
|
|
|
3102
3121
|
if (!r.ok) {
|
|
3103
3122
|
const tooLarge = r.error.code === "too_large";
|
|
3104
3123
|
const refusedAt = Date.now();
|
|
3105
|
-
if (tooLarge)
|
|
3124
|
+
if (tooLarge && !this.snapshotTooLargeRoots.has(root))
|
|
3106
3125
|
this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
|
|
3107
3126
|
try {
|
|
3108
3127
|
this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
|
|
@@ -57,4 +57,5 @@ export declare function createToolSearchTool(opts: {
|
|
|
57
57
|
listingRide?: (newlyActivated: readonly string[]) => string | undefined;
|
|
58
58
|
mountedNames?: () => ReadonlySet<string>;
|
|
59
59
|
directCallEnabled?: boolean;
|
|
60
|
+
serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
|
|
60
61
|
}): AgentTool;
|
|
@@ -172,11 +172,22 @@ export function resolveToolSearch(args, registry) {
|
|
|
172
172
|
export function extractDiscoveredToolNames(messages, registry) {
|
|
173
173
|
const names = new Set();
|
|
174
174
|
const pendingDirect = new Map();
|
|
175
|
+
const pendingSearch = new Map();
|
|
175
176
|
for (const m of messages) {
|
|
176
177
|
if (m.role === "toolResult") {
|
|
177
178
|
const name = pendingDirect.get(m.toolCallId);
|
|
178
|
-
if (name !== undefined
|
|
179
|
-
|
|
179
|
+
if (name !== undefined) {
|
|
180
|
+
pendingDirect.delete(m.toolCallId);
|
|
181
|
+
if (m.isError !== true)
|
|
182
|
+
names.add(name);
|
|
183
|
+
}
|
|
184
|
+
const searched = pendingSearch.get(m.toolCallId);
|
|
185
|
+
if (searched !== undefined) {
|
|
186
|
+
pendingSearch.delete(m.toolCallId);
|
|
187
|
+
if (m.isError !== true)
|
|
188
|
+
for (const n of searched)
|
|
189
|
+
names.add(n);
|
|
190
|
+
}
|
|
180
191
|
continue;
|
|
181
192
|
}
|
|
182
193
|
if (m.role !== "assistant")
|
|
@@ -185,15 +196,18 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
185
196
|
if (part.type !== "toolCall")
|
|
186
197
|
continue;
|
|
187
198
|
if (part.name === TOOL_SEARCH_NAME) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
const prior = pendingSearch.get(part.id);
|
|
200
|
+
const resolved = resolveToolSearch(part.arguments, registry);
|
|
201
|
+
pendingSearch.set(part.id, prior === undefined ? resolved : [...prior, ...resolved]);
|
|
191
202
|
}
|
|
192
203
|
else if (registry.has(part.name)) {
|
|
193
204
|
pendingDirect.set(part.id, part.name);
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
}
|
|
208
|
+
for (const list of pendingSearch.values())
|
|
209
|
+
for (const n of list)
|
|
210
|
+
names.add(n);
|
|
197
211
|
return [...names];
|
|
198
212
|
}
|
|
199
213
|
export function createToolSearchTool(opts) {
|
|
@@ -209,6 +223,12 @@ export function createToolSearchTool(opts) {
|
|
|
209
223
|
"first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
|
|
210
224
|
'with query "select:<name>" before calling it. ';
|
|
211
225
|
let activationChain = Promise.resolve();
|
|
226
|
+
const serializeActivation = opts.serializeActivation ??
|
|
227
|
+
((section) => {
|
|
228
|
+
const p = activationChain.then(section);
|
|
229
|
+
activationChain = p.then(() => undefined, () => undefined);
|
|
230
|
+
return p;
|
|
231
|
+
});
|
|
212
232
|
return defineTool({
|
|
213
233
|
name: TOOL_SEARCH_NAME,
|
|
214
234
|
contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
|
|
@@ -259,7 +279,7 @@ export function createToolSearchTool(opts) {
|
|
|
259
279
|
},
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
|
-
const section =
|
|
282
|
+
const section = serializeActivation(async () => {
|
|
263
283
|
const newly = matched.filter((n) => !active.has(n));
|
|
264
284
|
if (newly.length > 0) {
|
|
265
285
|
for (const n of newly)
|
|
@@ -275,7 +295,6 @@ export function createToolSearchTool(opts) {
|
|
|
275
295
|
}
|
|
276
296
|
return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
|
|
277
297
|
});
|
|
278
|
-
activationChain = section.then(() => undefined, () => undefined);
|
|
279
298
|
const { newly, ride } = await section;
|
|
280
299
|
const lines = matched.map((n) => {
|
|
281
300
|
const info = registry.get(n);
|
|
@@ -114,6 +114,11 @@ export class TtlSessionStore {
|
|
|
114
114
|
}
|
|
115
115
|
const forked = await this.repo.fork(await source.getMetadata(), forkParams);
|
|
116
116
|
const meta = await forked.getMetadata();
|
|
117
|
+
const inherited = this.owners.has(sourceId) ? this.owners.get(sourceId) : undefined;
|
|
118
|
+
const forkOwner = owner ?? inherited;
|
|
119
|
+
if (forkOwner !== undefined && !this.owners.has(meta.id)) {
|
|
120
|
+
this.owners.set(meta.id, forkOwner);
|
|
121
|
+
}
|
|
117
122
|
this.entries.set(meta.id, {
|
|
118
123
|
session: forked,
|
|
119
124
|
lastActiveAt: Date.now(),
|
|
@@ -149,8 +154,13 @@ export class TtlSessionStore {
|
|
|
149
154
|
return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined;
|
|
150
155
|
}
|
|
151
156
|
async register(sessionId, owner) {
|
|
152
|
-
if (
|
|
153
|
-
|
|
157
|
+
if (this.owners.has(sessionId)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (owner !== null && typeof owner !== "string") {
|
|
161
|
+
throw new TypeError(`SessionStore.register: owner must be a string (owned) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner}`);
|
|
162
|
+
}
|
|
163
|
+
this.owners.set(sessionId, owner);
|
|
154
164
|
}
|
|
155
165
|
sweep(now = Date.now()) {
|
|
156
166
|
for (const [id, e] of this.entries) {
|
|
@@ -160,8 +170,9 @@ export class TtlSessionStore {
|
|
|
160
170
|
if (now - e.lastActiveAt > this.defaultTtlMs) {
|
|
161
171
|
this.entries.delete(id);
|
|
162
172
|
if (this.evictPolicy === "delete") {
|
|
163
|
-
void this.repo.delete({ id, createdAt: "" })
|
|
164
|
-
|
|
173
|
+
void this.repo.delete({ id, createdAt: "" }).then(() => {
|
|
174
|
+
this.owners.delete(id);
|
|
175
|
+
}, () => { });
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SkillToolEntry {
|
|
2
|
+
readonly raw: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly specifier?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSkillToolEntry(entry: string): SkillToolEntry;
|
|
7
|
+
export declare function isSkillSpecifierEnforced(canonicalName: string): boolean;
|
|
8
|
+
export declare function skillSpecifierRejection(entry: SkillToolEntry, args: unknown): string | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parsePermissionRule, wildcardMatch } from "./permission-rules.js";
|
|
2
|
+
import { canonicalToolName } from "./tool-name-aliases.js";
|
|
3
|
+
import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
|
|
4
|
+
import { parseLeadingCommandName } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
|
+
const SPECIFIER_ENFORCED_TOOLS = new Set(COARSE_SHELL_TOOLS.map(canonicalToolName));
|
|
6
|
+
export function parseSkillToolEntry(entry) {
|
|
7
|
+
const parsed = parsePermissionRule(entry);
|
|
8
|
+
const name = canonicalToolName(parsed.toolName);
|
|
9
|
+
return parsed.ruleContent === undefined ? { raw: entry, name } : { raw: entry, name, specifier: parsed.ruleContent };
|
|
10
|
+
}
|
|
11
|
+
export function isSkillSpecifierEnforced(canonicalName) {
|
|
12
|
+
return SPECIFIER_ENFORCED_TOOLS.has(canonicalName);
|
|
13
|
+
}
|
|
14
|
+
function normalizeSpacing(s) {
|
|
15
|
+
return s.trim().replace(/[ \t]+/g, " ");
|
|
16
|
+
}
|
|
17
|
+
function hasUnescapedStar(content) {
|
|
18
|
+
for (let i = 0; i < content.length; i++) {
|
|
19
|
+
if (content[i] !== "*")
|
|
20
|
+
continue;
|
|
21
|
+
let backslashes = 0;
|
|
22
|
+
for (let j = i - 1; j >= 0 && content[j] === "\\"; j--)
|
|
23
|
+
backslashes++;
|
|
24
|
+
if (backslashes % 2 === 0)
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function specifierMatchesCommand(specifier, command) {
|
|
30
|
+
const spec = normalizeSpacing(specifier);
|
|
31
|
+
const prefix = /^(.+):\*$/.exec(spec)?.[1];
|
|
32
|
+
if (prefix !== undefined) {
|
|
33
|
+
return command === prefix || command.startsWith(prefix + " ");
|
|
34
|
+
}
|
|
35
|
+
if (hasUnescapedStar(spec))
|
|
36
|
+
return wildcardMatch(spec, command);
|
|
37
|
+
return command === spec;
|
|
38
|
+
}
|
|
39
|
+
export function skillSpecifierRejection(entry, args) {
|
|
40
|
+
const { specifier } = entry;
|
|
41
|
+
if (specifier === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!isSkillSpecifierEnforced(entry.name)) {
|
|
44
|
+
return `entry "${entry.raw}" narrows a tool whose calls this gate cannot match against a command pattern — only the shell tools (${[...SPECIFIER_ENFORCED_TOOLS].join(", ")}) carry a command string, so the entry admits nothing`;
|
|
45
|
+
}
|
|
46
|
+
const command = args?.command;
|
|
47
|
+
if (typeof command !== "string") {
|
|
48
|
+
return `entry "${entry.raw}" requires a command string to match against, and this call has none`;
|
|
49
|
+
}
|
|
50
|
+
const parsedCommand = parseLeadingCommandName(command);
|
|
51
|
+
if ("reject" in parsedCommand) {
|
|
52
|
+
return `entry "${entry.raw}" only admits a single simple command (${parsedCommand.reject})`;
|
|
53
|
+
}
|
|
54
|
+
if (!specifierMatchesCommand(specifier, normalizeSpacing(command))) {
|
|
55
|
+
return `command "${command.trim()}" is not admitted by entry "${entry.raw}"`;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillSpec } from "./types.js";
|
|
2
|
-
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
2
|
+
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "allowed_tool_pattern_unsupported" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
3
3
|
export interface SkillsDirectoryWarning {
|
|
4
4
|
code: SkillsDirectoryWarningCode;
|
|
5
5
|
skill: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { canonicalToolName } from "./tool-name-aliases.js";
|
|
4
|
+
import { isSkillSpecifierEnforced, parseSkillToolEntry } from "./skill-tool-specifier.js";
|
|
4
5
|
const SKILL_FILE = "SKILL.md";
|
|
5
6
|
const RESOURCE_DIRS = ["assets", "references", "scripts"];
|
|
6
7
|
const NAME_MAX_CHARS = 64;
|
|
@@ -316,12 +317,23 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
316
317
|
const names = [];
|
|
317
318
|
const seen = new Set();
|
|
318
319
|
for (const n of splitToolNames(declared)) {
|
|
319
|
-
const
|
|
320
|
+
const entry = parseSkillToolEntry(n);
|
|
321
|
+
const key = entry.specifier === undefined ? entry.name : `${entry.name}(${entry.specifier})`;
|
|
320
322
|
if (seen.has(key))
|
|
321
323
|
continue;
|
|
322
324
|
seen.add(key);
|
|
323
325
|
names.push(n);
|
|
324
326
|
}
|
|
327
|
+
for (const n of names) {
|
|
328
|
+
const entry = parseSkillToolEntry(n);
|
|
329
|
+
if (entry.specifier !== undefined && !isSkillSpecifierEnforced(entry.name)) {
|
|
330
|
+
warn({
|
|
331
|
+
code: "allowed_tool_pattern_unsupported",
|
|
332
|
+
skill: skillName,
|
|
333
|
+
detail: `allowed-tools entry "${n}" narrows a tool whose calls cannot be matched against a command pattern — only shell tools carry one, so the entry is not enforced and the tool is DISABLED inside this skill frame. Use the bare tool name to allow it fully.`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
325
337
|
if (names.length === 0)
|
|
326
338
|
return undefined;
|
|
327
339
|
if (names.some((n) => n === ALL_TOOLS_WILDCARD))
|
|
@@ -329,9 +341,9 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
329
341
|
let allowTools = names;
|
|
330
342
|
if (deployedTools !== undefined) {
|
|
331
343
|
const mounted = new Set(deployedTools.map(canonicalToolName));
|
|
332
|
-
allowTools = names.filter((n) => mounted.has(
|
|
344
|
+
allowTools = names.filter((n) => mounted.has(parseSkillToolEntry(n).name));
|
|
333
345
|
for (const n of names) {
|
|
334
|
-
if (!mounted.has(
|
|
346
|
+
if (!mounted.has(parseSkillToolEntry(n).name)) {
|
|
335
347
|
warn({
|
|
336
348
|
code: "allowed_tool_not_mounted",
|
|
337
349
|
skill: skillName,
|
|
@@ -342,7 +354,7 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
342
354
|
}
|
|
343
355
|
if (disallowed.length > 0) {
|
|
344
356
|
const denied = new Set(disallowed.map(canonicalToolName));
|
|
345
|
-
allowTools = allowTools.filter((n) => !denied.has(
|
|
357
|
+
allowTools = allowTools.filter((n) => !denied.has(parseSkillToolEntry(n).name));
|
|
346
358
|
}
|
|
347
359
|
return { allowTools, lineageId: `skill:${skillName}` };
|
|
348
360
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
|
|
3
|
-
import type { DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
|
|
3
|
+
import type { CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
|
|
4
4
|
import type { TaskNotificationPayload } from "./task-notification.js";
|
|
5
5
|
export type ModelRef = string | Model;
|
|
6
6
|
export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier";
|
|
@@ -13,10 +13,7 @@ export type RoleSpec = ModelRef | {
|
|
|
13
13
|
export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>;
|
|
14
14
|
export interface Brain {
|
|
15
15
|
stream: StreamFn;
|
|
16
|
-
complete?:
|
|
17
|
-
systemPrompt?: string;
|
|
18
|
-
messages: unknown[];
|
|
19
|
-
}, options?: unknown) => Promise<unknown>;
|
|
16
|
+
complete?: CompleteSimpleFn;
|
|
20
17
|
}
|
|
21
18
|
export type ToolEffect = "read" | "write" | "idempotent";
|
|
22
19
|
export interface ToolSpec<TParams extends TSchema = TSchema> {
|
package/dist/core/with-retry.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -188,7 +188,7 @@ export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type D
|
|
|
188
188
|
export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
|
|
189
189
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
190
190
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
191
|
-
export type { AssistantMessage, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
191
|
+
export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
192
192
|
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
|
|
193
193
|
export { Type } from "typebox";
|
|
194
194
|
export type { TSchema, Static } from "typebox";
|
package/dist/internal/llm.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createAssistantMessageEventStream, stripEngineMetadata } from "../engine/llm/index.js";
|
|
2
|
-
export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
|
|
2
|
+
export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
|
|
@@ -480,13 +480,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
480
480
|
if (row !== undefined)
|
|
481
481
|
row.sessionId = sessionId;
|
|
482
482
|
};
|
|
483
|
-
const bceTick = (callKey, e) => {
|
|
483
|
+
const bceTick = (callKey, e, fromSessionId) => {
|
|
484
484
|
if (!bceSink)
|
|
485
485
|
return;
|
|
486
486
|
const id = waIdOf(callKey);
|
|
487
487
|
const row = bceLive.get(id);
|
|
488
488
|
if (row === undefined)
|
|
489
489
|
return;
|
|
490
|
+
if (fromSessionId !== undefined && row.sessionId !== undefined && fromSessionId !== row.sessionId)
|
|
491
|
+
return;
|
|
490
492
|
bceEmit({
|
|
491
493
|
kind: "tick",
|
|
492
494
|
taskId: id,
|
|
@@ -507,8 +509,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
507
509
|
if (!bceSink)
|
|
508
510
|
return;
|
|
509
511
|
const id = waIdOf(callKey);
|
|
512
|
+
const row = bceLive.get(id);
|
|
510
513
|
if (!bceLive.delete(id))
|
|
511
514
|
return;
|
|
515
|
+
const coord = sessionId ?? row?.sessionId;
|
|
512
516
|
bceEmit({
|
|
513
517
|
kind: "terminal",
|
|
514
518
|
taskId: id,
|
|
@@ -516,7 +520,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
516
520
|
owner: runId,
|
|
517
521
|
workflowRunId: runId,
|
|
518
522
|
...(scope !== undefined ? { scope } : {}),
|
|
519
|
-
...(
|
|
523
|
+
...(coord !== undefined ? { sessionId: coord, transcriptId: coord } : {}),
|
|
520
524
|
status,
|
|
521
525
|
summary: boundedRedactedSummary(summary, 300),
|
|
522
526
|
...(stats !== undefined ? { usage: { tokens: stats.tokens, turns: stats.turns, ...(stats.costMicroUsd !== undefined ? { costMicroUsd: stats.costMicroUsd } : {}) } } : {}),
|
|
@@ -873,7 +877,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
873
877
|
? {
|
|
874
878
|
onForwardEvent: (e) => {
|
|
875
879
|
if (e.type === "task_progress")
|
|
876
|
-
bceTick(callKey, e);
|
|
880
|
+
bceTick(callKey, e, attemptSessionId);
|
|
877
881
|
try {
|
|
878
882
|
baseInternals.onForwardEvent?.(e);
|
|
879
883
|
}
|
|
@@ -1181,7 +1185,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1181
1185
|
? {
|
|
1182
1186
|
onForwardEvent: (e) => {
|
|
1183
1187
|
if (e.type === "task_progress")
|
|
1184
|
-
bceTick(callKey, e);
|
|
1188
|
+
bceTick(callKey, e, childSessionId);
|
|
1185
1189
|
try {
|
|
1186
1190
|
enrichedForwardS?.(e);
|
|
1187
1191
|
}
|
|
@@ -107,10 +107,42 @@ function hasUnquotedExpansionMetachar(rawToken) {
|
|
|
107
107
|
}
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
|
+
function longOptionNameOf(tok) {
|
|
111
|
+
if (!tok.startsWith("--") || tok.length === 2)
|
|
112
|
+
return undefined;
|
|
113
|
+
const body = tok.slice(2);
|
|
114
|
+
const eq = body.indexOf("=");
|
|
115
|
+
const name = eq >= 0 ? body.slice(0, eq) : body;
|
|
116
|
+
return name.length > 0 ? name : undefined;
|
|
117
|
+
}
|
|
118
|
+
function isLongOptionAbbrevOf(name, full) {
|
|
119
|
+
return full.startsWith(name);
|
|
120
|
+
}
|
|
110
121
|
function isGrepPatternFlagToken(tok) {
|
|
111
|
-
if (tok.startsWith("--"))
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
if (tok.startsWith("--")) {
|
|
123
|
+
const long = longOptionNameOf(tok);
|
|
124
|
+
return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
|
|
125
|
+
}
|
|
126
|
+
return /^-[A-Za-z0-9]*[ef]/.test(tok);
|
|
127
|
+
}
|
|
128
|
+
function isGrepPatternPayloadLongOption(tok) {
|
|
129
|
+
if (!tok.includes("="))
|
|
130
|
+
return false;
|
|
131
|
+
const name = longOptionNameOf(tok);
|
|
132
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
|
|
133
|
+
}
|
|
134
|
+
function isCutDelimiterPayloadLongOption(tok) {
|
|
135
|
+
if (!tok.includes("="))
|
|
136
|
+
return false;
|
|
137
|
+
const name = longOptionNameOf(tok);
|
|
138
|
+
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
139
|
+
}
|
|
140
|
+
function isGrepFileStdinLongOption(tok) {
|
|
141
|
+
const eq = tok.indexOf("=");
|
|
142
|
+
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
143
|
+
return false;
|
|
144
|
+
const name = longOptionNameOf(tok);
|
|
145
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "file");
|
|
114
146
|
}
|
|
115
147
|
function grepClusterValueOwner(tok) {
|
|
116
148
|
if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
|
|
@@ -176,9 +208,9 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
176
208
|
k++;
|
|
177
209
|
continue;
|
|
178
210
|
}
|
|
179
|
-
if (name === "cut" && (/^-d./.test(t) ||
|
|
211
|
+
if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
|
|
180
212
|
continue;
|
|
181
|
-
if (name === "grep" && (grepClusterValueOwner(t) === "e" ||
|
|
213
|
+
if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
|
|
182
214
|
continue;
|
|
183
215
|
if (t.startsWith("-") && t !== "-") {
|
|
184
216
|
for (const payload of attachedOptionPayloads(t)) {
|
|
@@ -312,7 +344,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
312
344
|
k++;
|
|
313
345
|
continue;
|
|
314
346
|
}
|
|
315
|
-
if (name === "grep" && (t === "-f-" || t
|
|
347
|
+
if (name === "grep" && (t === "-f-" || isGrepFileStdinLongOption(t))) {
|
|
316
348
|
hasStdinDash = true;
|
|
317
349
|
break;
|
|
318
350
|
}
|
|
@@ -158,6 +158,12 @@ export function createSchedulerTools(env, ctx) {
|
|
|
158
158
|
return [];
|
|
159
159
|
const sched = env;
|
|
160
160
|
const schedCtx = toSchedulerContext(ctx);
|
|
161
|
+
let cronCreateChain = Promise.resolve();
|
|
162
|
+
const serializedCronCreateOp = (fn) => {
|
|
163
|
+
const next = cronCreateChain.then(fn, fn);
|
|
164
|
+
cronCreateChain = next.then(() => undefined, () => undefined);
|
|
165
|
+
return next;
|
|
166
|
+
};
|
|
161
167
|
const cronCreateDescription = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders. The task runs UNATTENDED when it fires, so write a fully self-contained \`prompt\` (it will not see this conversation). Use CronList to see what you've scheduled, CronDelete to remove one.
|
|
162
168
|
|
|
163
169
|
## One-shot tasks (schedule kind "delay" or "at")
|
|
@@ -210,7 +216,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
210
216
|
label: Type.Optional(Type.String({ description: "Short label (also a dedup key with the schedule)." })),
|
|
211
217
|
}),
|
|
212
218
|
effect: "write",
|
|
213
|
-
execute: async (args) => {
|
|
219
|
+
execute: async (args) => serializedCronCreateOp(async () => {
|
|
214
220
|
const a = args;
|
|
215
221
|
if (a.schedule !== undefined && a.cron !== undefined) {
|
|
216
222
|
return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
|
|
@@ -224,7 +230,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
224
230
|
const when = a.schedule ?? { kind: "cron", expr: a.cron };
|
|
225
231
|
if (when.kind === "cron") {
|
|
226
232
|
if (!isValidCronExpr(when.expr)) {
|
|
227
|
-
return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5
|
|
233
|
+
return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5 space-separated cron fields (digits and * / , -) are allowed (a 6th leading seconds field is parsed but always rejected next — this scheduler has no seconds hand).`);
|
|
228
234
|
}
|
|
229
235
|
const deepErr = cronScheduleError(when.expr);
|
|
230
236
|
if (deepErr)
|
|
@@ -277,7 +283,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
277
283
|
...(replaced !== undefined ? { replaced } : {}),
|
|
278
284
|
},
|
|
279
285
|
};
|
|
280
|
-
},
|
|
286
|
+
}),
|
|
281
287
|
});
|
|
282
288
|
const cronCancel = defineTool({
|
|
283
289
|
name: "CronDelete",
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface WebFetchConfig {
|
|
|
6
6
|
fetchImpl?: typeof fetch;
|
|
7
7
|
maxBytes?: number;
|
|
8
8
|
timeoutMs?: number;
|
|
9
|
-
summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string
|
|
9
|
+
summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated?: boolean;
|
|
12
|
+
}>;
|
|
10
13
|
userAgent?: string;
|
|
11
14
|
}
|
|
12
15
|
export declare function htmlToText(html: string): string;
|
|
@@ -14,7 +17,10 @@ export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
|
|
|
14
17
|
export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
|
|
15
18
|
export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
|
|
16
19
|
export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
|
|
17
|
-
export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string
|
|
20
|
+
export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
21
|
+
text: string;
|
|
22
|
+
truncated?: boolean;
|
|
23
|
+
}>;
|
|
18
24
|
export interface WebSearchConfig {
|
|
19
25
|
search: (query: string, signal?: AbortSignal, opts?: {
|
|
20
26
|
allowedDomains?: string[];
|
package/dist/tools/web.js
CHANGED
|
@@ -495,6 +495,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
495
495
|
}
|
|
496
496
|
const text = /html/i.test(contentType) || /^\s*</.test(raw) ? htmlToText(raw) : raw;
|
|
497
497
|
let out = text;
|
|
498
|
+
let summaryTruncated = false;
|
|
498
499
|
let note;
|
|
499
500
|
if (prompt && bodyCut) {
|
|
500
501
|
note =
|
|
@@ -503,7 +504,17 @@ export function webFetchToolSpec(config = {}) {
|
|
|
503
504
|
}
|
|
504
505
|
else if (prompt && config.summarize) {
|
|
505
506
|
try {
|
|
506
|
-
|
|
507
|
+
const summarized = await config.summarize(text, prompt, ctx.signal);
|
|
508
|
+
if (typeof summarized === "string") {
|
|
509
|
+
out = summarized;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
out = summarized.text;
|
|
513
|
+
if (summarized.truncated) {
|
|
514
|
+
summaryTruncated = true;
|
|
515
|
+
note = `[note: the summary below is INCOMPLETE — the summarizer hit its output limit before finishing]`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
507
518
|
}
|
|
508
519
|
catch (e) {
|
|
509
520
|
out = text;
|
|
@@ -534,7 +545,7 @@ export function webFetchToolSpec(config = {}) {
|
|
|
534
545
|
bytes: bodyBytes ? bodyBytes.length : raw.length,
|
|
535
546
|
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,
|
|
536
547
|
durationMs: Date.now() - startedAt,
|
|
537
|
-
...(truncationNote ? { truncated: true } : {}),
|
|
548
|
+
...(truncationNote || summaryTruncated ? { truncated: true } : {}),
|
|
538
549
|
...transferStateDetails,
|
|
539
550
|
},
|
|
540
551
|
};
|
|
@@ -560,18 +571,17 @@ export function createWebFetchSummarizer(brain, model) {
|
|
|
560
571
|
const msg = brain.complete
|
|
561
572
|
? await brain.complete(model, context, { signal })
|
|
562
573
|
: await (await Promise.resolve(brain.stream(model, context, { signal }))).result();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
throw new Error(am.errorMessage ?? `summarizer stopped with ${am.stopReason}`);
|
|
574
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
575
|
+
throw new Error(msg.errorMessage ?? `summarizer stopped with ${msg.stopReason}`);
|
|
566
576
|
}
|
|
567
|
-
const text =
|
|
568
|
-
.filter((b) => b
|
|
577
|
+
const text = msg.content
|
|
578
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
569
579
|
.map((b) => b.text)
|
|
570
580
|
.join("\n")
|
|
571
581
|
.trim();
|
|
572
582
|
if (!text)
|
|
573
583
|
throw new Error("summarizer returned no text");
|
|
574
|
-
return text;
|
|
584
|
+
return msg.stopReason === "length" || msg.partialFinalized === true ? { text, truncated: true } : text;
|
|
575
585
|
};
|
|
576
586
|
}
|
|
577
587
|
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|
|
@@ -801,25 +811,27 @@ export function createWebSearchTool(config) {
|
|
|
801
811
|
export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
802
812
|
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
803
813
|
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
804
|
-
const
|
|
814
|
+
const parsedBase = new URL(baseUrl);
|
|
815
|
+
parsedBase.hash = "";
|
|
816
|
+
parsedBase.pathname = `${parsedBase.pathname.replace(/\/+$/, "")}/search`;
|
|
805
817
|
return async (query, signal, opts) => {
|
|
806
818
|
const q = opts?.allowedDomains && opts.allowedDomains.length > 0
|
|
807
819
|
? `${opts.allowedDomains.map((d) => `site:${d}`).join(" OR ")} ${query}`
|
|
808
820
|
: query;
|
|
809
|
-
const url = new URL(
|
|
810
|
-
url.searchParams.set("q", q);
|
|
811
|
-
url.searchParams.set("format", "json");
|
|
821
|
+
const url = new URL(parsedBase);
|
|
812
822
|
for (const [k, v] of Object.entries(options.extraParams ?? {}))
|
|
813
823
|
url.searchParams.set(k, v);
|
|
824
|
+
url.searchParams.set("q", q);
|
|
825
|
+
url.searchParams.set("format", "json");
|
|
814
826
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
815
827
|
const res = await doFetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout, headers: { accept: "application/json" } });
|
|
816
828
|
if (!res.ok)
|
|
817
|
-
throw new Error(`SearXNG ${res.status} ${res.statusText} from ${
|
|
829
|
+
throw new Error(`SearXNG ${res.status} ${res.statusText} from ${url.origin}${url.pathname}`);
|
|
818
830
|
const body = (await res.json());
|
|
819
831
|
if (!Array.isArray(body.results))
|
|
820
|
-
throw new Error(`SearXNG returned no results array from ${
|
|
832
|
+
throw new Error(`SearXNG returned no results array from ${url.origin}${url.pathname} — is format=json enabled on this instance?`);
|
|
821
833
|
return body.results
|
|
822
|
-
.filter((r) => typeof r.url === "string" && r.url !== "")
|
|
834
|
+
.filter((r) => typeof r === "object" && r !== null && typeof r.url === "string" && r.url !== "")
|
|
823
835
|
.map((r) => ({
|
|
824
836
|
title: typeof r.title === "string" && r.title !== "" ? r.title : r.url,
|
|
825
837
|
url: r.url,
|
|
@@ -829,14 +841,31 @@ export function createSearxngSearchBackend(baseUrl, options = {}) {
|
|
|
829
841
|
}
|
|
830
842
|
export async function probeSearchBackend(search, options) {
|
|
831
843
|
const budget = options?.timeoutMs ?? 15_000;
|
|
844
|
+
let timer;
|
|
832
845
|
try {
|
|
833
846
|
const results = await Promise.race([
|
|
834
847
|
search("connectivity probe", AbortSignal.timeout(budget)),
|
|
835
|
-
new Promise((_, reject) =>
|
|
848
|
+
new Promise((_, reject) => {
|
|
849
|
+
timer = setTimeout(() => reject(new Error(`probe timed out after ${budget}ms`)), budget);
|
|
850
|
+
}),
|
|
836
851
|
]);
|
|
837
852
|
return { ok: true, results: results.length };
|
|
838
853
|
}
|
|
839
854
|
catch (e) {
|
|
840
|
-
|
|
855
|
+
let text;
|
|
856
|
+
if (e instanceof Error)
|
|
857
|
+
text = e.message;
|
|
858
|
+
else {
|
|
859
|
+
try {
|
|
860
|
+
text = String(e);
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
text = Object.prototype.toString.call(e);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { ok: false, error: text };
|
|
867
|
+
}
|
|
868
|
+
finally {
|
|
869
|
+
clearTimeout(timer);
|
|
841
870
|
}
|
|
842
871
|
}
|