@foldrun/core 2.7.0 → 2.8.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/src/delegate-tools.d.ts +45 -0
- package/dist/src/delegate-tools.js +153 -0
- package/dist/src/egress.d.ts +27 -2
- package/dist/src/frontmatter-fields.js +2 -1
- package/dist/src/oauth-presets.d.ts +7 -0
- package/dist/src/oauth-presets.js +7 -0
- package/dist/src/operator.d.ts +19 -0
- package/dist/src/operator.js +29 -0
- package/dist/src/platform.d.ts +8 -0
- package/dist/src/run-container.d.ts +9 -2
- package/dist/src/run-container.js +8 -2
- package/dist/src/runner.d.ts +21 -1
- package/dist/src/runner.js +288 -11
- package/dist/src/scan.js +17 -1
- package/dist/src/starter.js +1 -0
- package/dist/src/store.d.ts +34 -0
- package/dist/src/store.js +12 -0
- package/dist/src/subagents.d.ts +14 -0
- package/dist/src/subagents.js +8 -0
- package/package.json +1 -1
- package/src/delegate-tools.ts +175 -0
- package/src/egress.ts +24 -3
- package/src/frontmatter-fields.ts +2 -1
- package/src/oauth-presets.ts +7 -0
- package/src/operator.ts +52 -0
- package/src/platform.ts +5 -0
- package/src/run-container.ts +16 -3
- package/src/runner.ts +291 -9
- package/src/scan.ts +19 -1
- package/src/starter.ts +1 -0
- package/src/store.ts +38 -0
- package/src/subagents.ts +19 -0
package/dist/src/runner.js
CHANGED
|
@@ -6,6 +6,7 @@ import { latestRevisionId } from "./history.js";
|
|
|
6
6
|
import { recordCallIo, redactionsFor } from "./capture.js";
|
|
7
7
|
import { AUTH_REFUSAL } from "./issues.js";
|
|
8
8
|
import { hookNames } from "./hooks.js";
|
|
9
|
+
import crypto from "node:crypto";
|
|
9
10
|
import fs from "node:fs";
|
|
10
11
|
import os from "node:os";
|
|
11
12
|
import path from "node:path";
|
|
@@ -32,12 +33,13 @@ const isolatedRun = () => {
|
|
|
32
33
|
return mode === "container" || (!!mode && mode in platform.isolation);
|
|
33
34
|
};
|
|
34
35
|
import { gatherConsults, buildConsultTools } from "./agent-tools.js";
|
|
35
|
-
import { DELEGATE_TOOL, gatherSubagents, subagentPrompt, subagentTools } from "./subagents.js";
|
|
36
|
+
import { DELEGATE_TOOL, gatherSubagents, ownSandbox, subagentPrompt, subagentTools } from "./subagents.js";
|
|
37
|
+
import { DELEGATE_SERVER, buildDelegateTools, delegatePromptLine, delegateToolNames, localDelegateChannel } from "./delegate-tools.js";
|
|
36
38
|
import { ASK_TOOL, PEER_TOOLS, applyOperatorEvent, askPromptLine, askTimeoutSec, buildAskTool, buildPeerTools, inboxReader, localPeerBus, peersPromptLine } from "./operator.js";
|
|
37
39
|
import { sendPlainNotification } from "./notify.js";
|
|
38
40
|
import { TOOL_MAP, BUILTIN_TOOLS, ownToolNames, toolRefs, legacyUseNames, legacyUseError, retiredToolNames, retiredToolError } from "./tool-names.js";
|
|
39
41
|
import { refNames } from "./refs.js";
|
|
40
|
-
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun,
|
|
42
|
+
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, isSplicedRecord, runSummary, verdictOf, sumAttempts, tokensOf, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
41
43
|
import { canReadBack } from "./tool-output.js";
|
|
42
44
|
import { PromptLayers, promptCacheKey } from "./prompt-layers.js";
|
|
43
45
|
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, tokenPriceOf } from "./catalog.js";
|
|
@@ -525,6 +527,10 @@ export function buildSubagentSpecs(o) {
|
|
|
525
527
|
try {
|
|
526
528
|
const tags = o.tags ?? [];
|
|
527
529
|
const own = agentContext(sub.dir, o.tenant, tags, { runId: o.runId, agent: sub.name }, o.flowTimezone, true);
|
|
530
|
+
// sandbox: own — delegated to as a step of its own, not inside this
|
|
531
|
+
// one (buildDelegateSpecs).
|
|
532
|
+
if (ownSandbox(own.front))
|
|
533
|
+
continue;
|
|
528
534
|
const description = typeof own.front.description === "string" ? own.front.description.trim() : "";
|
|
529
535
|
if (!description) {
|
|
530
536
|
o.push("error", `subagents: "${sub.name}" has no description — the model picks a sub-agent by it; not delegated to`);
|
|
@@ -556,6 +562,51 @@ export function buildSubagentSpecs(o) {
|
|
|
556
562
|
}
|
|
557
563
|
return out;
|
|
558
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* The `sandbox: own` sub-agents a step may hand jobs to: a name and a
|
|
567
|
+
* description each, all the parent's sandbox needs. What they may hold is
|
|
568
|
+
* read from their own file when a job arrives — never cut to the parent's,
|
|
569
|
+
* and never seen by it. `canRun` is false where nothing can start their
|
|
570
|
+
* sandbox (a sub-agent's own step: depth one; a step outside a run).
|
|
571
|
+
*/
|
|
572
|
+
export function buildDelegateSpecs(o) {
|
|
573
|
+
const out = [];
|
|
574
|
+
for (const sub of gatherSubagents(o.workspaceRoot, o.names).found) {
|
|
575
|
+
if (sub.name === o.self)
|
|
576
|
+
continue; // buildSubagentSpecs says so
|
|
577
|
+
let front;
|
|
578
|
+
try {
|
|
579
|
+
front = matter(fs.readFileSync(path.join(sub.dir, "agent.md"), "utf8")).data;
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
continue; // buildSubagentSpecs says so
|
|
583
|
+
}
|
|
584
|
+
if (!ownSandbox(front))
|
|
585
|
+
continue;
|
|
586
|
+
const description = typeof front.description === "string" ? front.description.trim() : "";
|
|
587
|
+
if (!description) {
|
|
588
|
+
o.push("error", `subagents: "${sub.name}" has no description — the model picks a sub-agent by it; not delegated to`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (!o.canRun) {
|
|
592
|
+
o.push("info", `subagents: "${sub.name}" runs in its own sandbox, which nothing here can start (a sub-agent cannot delegate again, and a step outside a run has no room for it) — not offered`);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (o.outward) {
|
|
596
|
+
let acts = true; // unreadable is treated as able to act: the safe side
|
|
597
|
+
const tags = o.tags ?? [];
|
|
598
|
+
try {
|
|
599
|
+
acts = agentContext(sub.dir, o.tenant ?? "default", tags, { agent: sub.name }, null, true).outward;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
/* as above */
|
|
603
|
+
}
|
|
604
|
+
o.outward(sub.name, acts);
|
|
605
|
+
}
|
|
606
|
+
out.push({ name: sub.name, description });
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
559
610
|
export function sharedInstructions(agentDir, tenant) {
|
|
560
611
|
const sections = [
|
|
561
612
|
["Everyone in this account", readAgentsMd(accountDir(tenant))?.body],
|
|
@@ -1852,7 +1903,11 @@ stepIndex,
|
|
|
1852
1903
|
siblings = [],
|
|
1853
1904
|
/** The in-process colleagues' bus for this group (localPeerBus), when
|
|
1854
1905
|
* the step runs in this process rather than a pod. */
|
|
1855
|
-
peerBus
|
|
1906
|
+
peerBus,
|
|
1907
|
+
/** Runs one job for a `sandbox: own` sub-agent as a step of its own and
|
|
1908
|
+
* answers with its reply (the drive loop's delegateFor). Absent for a
|
|
1909
|
+
* step that is itself a sub-agent's: depth one. */
|
|
1910
|
+
delegateJob) {
|
|
1856
1911
|
// Secret values are injected into scripts as environment variables and
|
|
1857
1912
|
// substituted into API headers, so a model that reads one back — from a
|
|
1858
1913
|
// script that prints it, an API error that echoes it, or its own `env` —
|
|
@@ -1872,6 +1927,9 @@ peerBus) {
|
|
|
1872
1927
|
let fileDir = null;
|
|
1873
1928
|
let installDir = null;
|
|
1874
1929
|
let disposeRuntime;
|
|
1930
|
+
/** The delegate door served for this step (sandbox: own sub-agents), held
|
|
1931
|
+
* here so the finally can close it however the step ends. */
|
|
1932
|
+
let serving = null;
|
|
1875
1933
|
// Can this step's tools act outside the workspace? Set once its context is
|
|
1876
1934
|
// read; the verdict at the end of this function reads it.
|
|
1877
1935
|
let canActOutward = false;
|
|
@@ -2323,8 +2381,13 @@ peerBus) {
|
|
|
2323
2381
|
// Colleagues this agent may DELEGATE to: each with its own context and
|
|
2324
2382
|
// its own tools — resolved exactly as its own step's would be, then cut
|
|
2325
2383
|
// to what THIS step holds (subagents.ts). Built host-side, like consults.
|
|
2384
|
+
// A sandbox: own job is a sub-agent already: depth one, so neither kind
|
|
2385
|
+
// of delegation is offered to it.
|
|
2386
|
+
if (step.subagentOf && refNames(front.subagents).length) {
|
|
2387
|
+
push("info", `subagents: not offered — ${step.agent} is running a job for ${step.subagentOf}, and a sub-agent does not delegate again`);
|
|
2388
|
+
}
|
|
2326
2389
|
const subagents = buildSubagentSpecs({
|
|
2327
|
-
names: front.subagents, self: step.agent, workspaceRoot, tenant, tags, runId, flowTimezone,
|
|
2390
|
+
names: step.subagentOf ? [] : front.subagents, self: step.agent, workspaceRoot, tenant, tags, runId, flowTimezone,
|
|
2328
2391
|
parentAllowed: [...allowed, ...consultNames.map((n) => `mcp__${n}`)], push,
|
|
2329
2392
|
});
|
|
2330
2393
|
if (subagents.length) {
|
|
@@ -2334,6 +2397,34 @@ peerBus) {
|
|
|
2334
2397
|
for (const s of subagents)
|
|
2335
2398
|
push("info", `subagent: ${s.name} — ${s.tools.length ? s.tools.join(", ") : "no tools"}`);
|
|
2336
2399
|
}
|
|
2400
|
+
// sandbox: own sub-agents: a delegate_<name> tool each. Their jobs run as
|
|
2401
|
+
// steps of their own (delegate-tools.ts); dropped again below where no
|
|
2402
|
+
// door to the worker exists.
|
|
2403
|
+
let delegates = buildDelegateSpecs({
|
|
2404
|
+
names: step.subagentOf ? [] : front.subagents, self: step.agent, workspaceRoot, canRun: !!delegateJob && stepIndex !== undefined && stepIndex >= 0, push,
|
|
2405
|
+
tenant,
|
|
2406
|
+
tags,
|
|
2407
|
+
outward: (_name, acts) => void (canActOutward ||= acts),
|
|
2408
|
+
});
|
|
2409
|
+
const offerDelegates = (specs) => {
|
|
2410
|
+
for (const n of delegateToolNames(delegates))
|
|
2411
|
+
if (allowed.includes(n))
|
|
2412
|
+
allowed.splice(allowed.indexOf(n), 1);
|
|
2413
|
+
if (consultNames.includes(DELEGATE_SERVER))
|
|
2414
|
+
consultNames.splice(consultNames.indexOf(DELEGATE_SERVER), 1);
|
|
2415
|
+
delegates = specs;
|
|
2416
|
+
if (!specs.length)
|
|
2417
|
+
return;
|
|
2418
|
+
allowed.push(...delegateToolNames(specs));
|
|
2419
|
+
consultNames.push(DELEGATE_SERVER);
|
|
2420
|
+
};
|
|
2421
|
+
offerDelegates(delegates);
|
|
2422
|
+
const delegateLine = delegates.length ? `\n\n${delegatePromptLine(delegates)}` : "";
|
|
2423
|
+
if (delegates.length) {
|
|
2424
|
+
prompt += delegateLine;
|
|
2425
|
+
for (const d of delegates)
|
|
2426
|
+
push("info", `subagent: ${d.name} — own sandbox, its own tools, secrets and network`);
|
|
2427
|
+
}
|
|
2337
2428
|
// The last provider refusal this step saw, for the fallback decision —
|
|
2338
2429
|
// watched on both paths, because a refusal reads the same from a pod
|
|
2339
2430
|
// and from this process.
|
|
@@ -2519,7 +2610,30 @@ peerBus) {
|
|
|
2519
2610
|
// colleague's message may be addressed to. Only when granted: a
|
|
2520
2611
|
// step without the tools has no reason to know its siblings.
|
|
2521
2612
|
...(peers && stepIndex !== undefined && stepIndex >= 0 ? { agent: step.agent, peers: siblings } : {}),
|
|
2613
|
+
// sandbox: own sub-agents: the only names the delegate door takes.
|
|
2614
|
+
...(delegates.length ? { delegates: delegates.map((d) => d.name) } : {}),
|
|
2522
2615
|
});
|
|
2616
|
+
// Serve those jobs while this step runs: each one becomes a step of
|
|
2617
|
+
// its own (delegateJob), and the door answers the long-poll with its
|
|
2618
|
+
// reply. No proxy, or a platform without the door: not offered — the
|
|
2619
|
+
// tools would only fail.
|
|
2620
|
+
if (delegates.length) {
|
|
2621
|
+
// Also: the executor must be able to stop ONE job — record its
|
|
2622
|
+
// sandbox (the resumable ones checkpoint it) and destroy it — or a job
|
|
2623
|
+
// whose parent stopped waiting would run on, sending and writing.
|
|
2624
|
+
const canStopAJob = !!platform.killSandbox && platform.sandboxResumable(isolation);
|
|
2625
|
+
if (lease && platform.egress.delegations && delegateJob && stepIndex !== undefined && canStopAJob) {
|
|
2626
|
+
serving = platform.egress.delegations(
|
|
2627
|
+
// Re-attaching to a sandbox that outlived its driver: what it
|
|
2628
|
+
// queued before the restart is still its own, not a dead attempt's.
|
|
2629
|
+
{ tenant, runId: runId ?? "adhoc", step: stepIndex, resuming: !!step.sandbox && step.sandbox.kind === isolation }, (j) => delegateJob(j.agent, j.job, j.cancelled ?? (() => false)));
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
push("error", `subagents: ${delegates.map((d) => d.name).join(", ")} run in their own sandbox, and this install ${canStopAJob ? "has no door to start one from inside a step" : "cannot stop one job's sandbox on its own"} — not offered`);
|
|
2633
|
+
offerDelegates([]);
|
|
2634
|
+
prompt = prompt.replace(delegateLine, "");
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2523
2637
|
// The forward door: every program in the sandbox sends through the
|
|
2524
2638
|
// proxy, which holds the site list and fills bound secrets. A proxy
|
|
2525
2639
|
// without the door (an older platform) leaves programs going direct.
|
|
@@ -2689,6 +2803,7 @@ peerBus) {
|
|
|
2689
2803
|
consults,
|
|
2690
2804
|
...(peers ? { peers: { self: step.agent, siblings } } : {}),
|
|
2691
2805
|
...(subagents.length ? { subagents } : {}),
|
|
2806
|
+
...(delegates.length ? { delegates } : {}),
|
|
2692
2807
|
timeoutSec: step.timeout,
|
|
2693
2808
|
budgetUsd: stepBudgetUsd,
|
|
2694
2809
|
budgetNote: stepBudgetNote,
|
|
@@ -2905,6 +3020,8 @@ peerBus) {
|
|
|
2905
3020
|
// to decide; a second go could send or charge twice.
|
|
2906
3021
|
step.image = outcome.image ?? null;
|
|
2907
3022
|
step.browserPod = outcome.browserPod ? { ...outcome.browserPod } : null;
|
|
3023
|
+
if (step.subagentOf && outcome.applied?.length)
|
|
3024
|
+
step.wrote = [...new Set([...(step.wrote ?? []), ...outcome.applied])];
|
|
2908
3025
|
if (outcome.status === "failed" && outcome.browserPod?.lost) {
|
|
2909
3026
|
const decision = podLossDecision(outcome.browserPod);
|
|
2910
3027
|
if (decision.rerun) {
|
|
@@ -2952,6 +3069,8 @@ peerBus) {
|
|
|
2952
3069
|
else
|
|
2953
3070
|
pushWatching("info", line.text, { effect: line.effect });
|
|
2954
3071
|
}
|
|
3072
|
+
await serving?.close();
|
|
3073
|
+
serving = null;
|
|
2955
3074
|
await lease.release();
|
|
2956
3075
|
}
|
|
2957
3076
|
// Nothing a test run left in storage/public/ reached storage/, so
|
|
@@ -3045,6 +3164,16 @@ peerBus) {
|
|
|
3045
3164
|
const peerChannel = peerBus && stepIndex !== undefined && stepIndex >= 0 ? peerBus.channelFor({ agent: step.agent, step: stepIndex }, siblings) : null;
|
|
3046
3165
|
const peerTools = peers ? buildPeerTools({ channel: peerChannel, self: step.agent, siblings, emit: push }) : null;
|
|
3047
3166
|
const localInbox = peerChannel ? inboxReader(peerChannel, push) : undefined;
|
|
3167
|
+
// sandbox: own sub-agents, here: no proxy between the step and this
|
|
3168
|
+
// process, so the job is handed to the drive loop directly.
|
|
3169
|
+
const delegateTools = delegates.length && delegateJob
|
|
3170
|
+
? buildDelegateTools({ specs: delegates, channel: localDelegateChannel(delegateJob), emit: push, timeoutSec: step.timeout })
|
|
3171
|
+
: null;
|
|
3172
|
+
// Said, not hidden: on this machine no step has a sandbox, so a job is
|
|
3173
|
+
// its own context, tools and secrets — but the same files and network.
|
|
3174
|
+
if (delegateTools?.server) {
|
|
3175
|
+
push("info", `subagents: ${delegates.map((d) => d.name).join(", ")} run here in this process — their own tools and secrets, but no sandbox: the same files and network as this step (on the platform each job gets a sandbox of its own)`);
|
|
3176
|
+
}
|
|
3048
3177
|
// @file secrets become 0600 paths here (host run), cleaned up in the
|
|
3049
3178
|
// finally — once, however many attempts follow.
|
|
3050
3179
|
const mat = materializeFileSecrets(agentDir, stepSecrets);
|
|
@@ -3086,6 +3215,7 @@ peerBus) {
|
|
|
3086
3215
|
...(deskTools.server ? { foldrun_desks: deskTools.server } : {}),
|
|
3087
3216
|
...(askTools ? { foldrun_ask: askTools.server } : {}),
|
|
3088
3217
|
...(peerTools ? { foldrun_peers: peerTools.server } : {}),
|
|
3218
|
+
...(delegateTools?.server ? { foldrun_delegate: delegateTools.server } : {}),
|
|
3089
3219
|
...(installTools ? { foldrun_install: installTools.server } : {}),
|
|
3090
3220
|
...mcpServers,
|
|
3091
3221
|
},
|
|
@@ -3209,6 +3339,16 @@ peerBus) {
|
|
|
3209
3339
|
push("error", err instanceof Error ? err.message : String(err));
|
|
3210
3340
|
}
|
|
3211
3341
|
finally {
|
|
3342
|
+
// The delegate door's service, when the step ended by throwing before
|
|
3343
|
+
// its lease was released: its jobs are settled, not left hanging.
|
|
3344
|
+
if (serving) {
|
|
3345
|
+
try {
|
|
3346
|
+
await serving.close();
|
|
3347
|
+
}
|
|
3348
|
+
catch {
|
|
3349
|
+
// best-effort — the door's records expire on their own
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3212
3352
|
try {
|
|
3213
3353
|
cleanupFileSecrets(fileDir);
|
|
3214
3354
|
if (installDir)
|
|
@@ -4014,6 +4154,32 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4014
4154
|
// last driver got, runStep passes that as `resume`, and the step
|
|
4015
4155
|
// carries on from where it was — a deploy no longer costs the run.
|
|
4016
4156
|
let orphaned = false;
|
|
4157
|
+
// A sandbox: own job whose driver went away is over: the reply it was
|
|
4158
|
+
// making went to a process that no longer exists, and the step that
|
|
4159
|
+
// asked is told it failed (the platform settles the door's side), so
|
|
4160
|
+
// its model can hand the job over again. Never resumed or relaunched.
|
|
4161
|
+
for (const step of run.steps) {
|
|
4162
|
+
if (!step.subagentOf || (step.status !== "running" && step.status !== "pending"))
|
|
4163
|
+
continue;
|
|
4164
|
+
// Its sandbox may still be executing — outward work included — while
|
|
4165
|
+
// the parent hands the job over again: destroy it before letting go
|
|
4166
|
+
// of the only reference to it.
|
|
4167
|
+
if (step.sandbox?.ref) {
|
|
4168
|
+
try {
|
|
4169
|
+
platform.killSandbox?.(step.sandbox);
|
|
4170
|
+
}
|
|
4171
|
+
catch {
|
|
4172
|
+
// a sandbox we cannot reach must not stop the run from resuming
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
step.status = "failed";
|
|
4176
|
+
step.sandbox = null;
|
|
4177
|
+
step.events.push({ t: new Date().toISOString(), type: "error", text: "interrupted — the worker driving this job restarted; the step that asked for it was told it failed" });
|
|
4178
|
+
const parent = run.steps.find((s) => s.delegations?.some((d) => d.id === step.delegation));
|
|
4179
|
+
const entry = parent?.delegations?.find((d) => d.id === step.delegation);
|
|
4180
|
+
if (entry && entry.status === "running")
|
|
4181
|
+
entry.status = "failed";
|
|
4182
|
+
}
|
|
4017
4183
|
for (const step of run.steps) {
|
|
4018
4184
|
if (step.status !== "running")
|
|
4019
4185
|
continue;
|
|
@@ -4158,7 +4324,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4158
4324
|
// three handoffs below read the flow's steps only: counting the rescue
|
|
4159
4325
|
// too handed the next group the same output twice, or a stale one
|
|
4160
4326
|
// from an evaluator loop's earlier cycle.
|
|
4161
|
-
const flowStepsOf = (group) => group.filter((s) => !
|
|
4327
|
+
const flowStepsOf = (group) => group.filter((s) => !isSplicedRecord(s));
|
|
4162
4328
|
const dataOf = (all) => {
|
|
4163
4329
|
const group = flowStepsOf(all);
|
|
4164
4330
|
const carrying = group.filter((s) => s.status === "completed" && s.data !== undefined);
|
|
@@ -4557,7 +4723,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4557
4723
|
save();
|
|
4558
4724
|
if (freshGroup.every((s) => s.status !== "pending")) {
|
|
4559
4725
|
// everything in this group was rejected
|
|
4560
|
-
if (freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !
|
|
4726
|
+
if (freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !isSplicedRecord(s))) {
|
|
4561
4727
|
run.status = "failed";
|
|
4562
4728
|
break;
|
|
4563
4729
|
}
|
|
@@ -4574,13 +4740,119 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4574
4740
|
// of the flow: one left pending by a drive that died is picked up by
|
|
4575
4741
|
// rescueStep below, never launched alongside the group (21 Oct 2026:
|
|
4576
4742
|
// a resumed run launched 77 leftover fixers at once).
|
|
4577
|
-
const launching = freshGroup.filter((s) => s.status === "pending" && !
|
|
4743
|
+
const launching = freshGroup.filter((s) => s.status === "pending" && !isSplicedRecord(s));
|
|
4578
4744
|
// tools: [peers] — what each step of the group is told about the
|
|
4579
4745
|
// others, and the bus they share when they run in this process.
|
|
4580
4746
|
const peerBus = localPeerBus();
|
|
4581
4747
|
const siblingsOf = (step) => launching
|
|
4582
4748
|
.filter((s) => s !== step)
|
|
4583
4749
|
.map((s) => ({ agent: s.agent, step: run.steps.indexOf(s), instruction: (s.instruction ?? "").split("\n")[0].trim().slice(0, 200) }));
|
|
4750
|
+
// sandbox: own sub-agents (delegate-tools.ts): one job, run as a step
|
|
4751
|
+
// of its own — its own sandbox, tools, secrets and network, from its
|
|
4752
|
+
// own file — and answered with its reply. The record is APPENDED to
|
|
4753
|
+
// run.steps, in the parent's group: every running step's lease and
|
|
4754
|
+
// inbox are addressed by index, so nothing may shift under them.
|
|
4755
|
+
// Marked subagentOf, it is never launched, handed on or reset as a
|
|
4756
|
+
// step of the flow (isSplicedRecord). It gets no delegateJob of its
|
|
4757
|
+
// own: depth one.
|
|
4758
|
+
const delegateFor = (parent) => async (agent, job, cancelled) => {
|
|
4759
|
+
const dir = path.join(pDir, "agents", agent);
|
|
4760
|
+
if (!fs.existsSync(path.join(dir, "agent.md")))
|
|
4761
|
+
return { status: "failed", result: `${agent} is not an agent in this workspace` };
|
|
4762
|
+
// A stable id on both records: an index into run.steps moves when a
|
|
4763
|
+
// rescue or a fan-out is spliced in before the child.
|
|
4764
|
+
const id = `d_${crypto.randomBytes(5).toString("hex")}`;
|
|
4765
|
+
const child = {
|
|
4766
|
+
agent,
|
|
4767
|
+
subagentOf: parent.agent,
|
|
4768
|
+
delegation: id,
|
|
4769
|
+
instruction: job,
|
|
4770
|
+
group: parent.group,
|
|
4771
|
+
optional: true,
|
|
4772
|
+
timeout: parent.timeout,
|
|
4773
|
+
attempts: 1,
|
|
4774
|
+
status: "running",
|
|
4775
|
+
events: [],
|
|
4776
|
+
result: null,
|
|
4777
|
+
costUsd: null,
|
|
4778
|
+
};
|
|
4779
|
+
run.steps.push(child);
|
|
4780
|
+
const at = run.steps.length - 1;
|
|
4781
|
+
const entry = { id, agent, status: "running" };
|
|
4782
|
+
(parent.delegations ??= []).push(entry);
|
|
4783
|
+
const started = Date.now();
|
|
4784
|
+
// The same budget rules as a step of the flow: nothing left means
|
|
4785
|
+
// not started (a zero ceiling would read as no cap at all), and
|
|
4786
|
+
// what it may spend is reserved while it runs, so two jobs at once
|
|
4787
|
+
// do not each get the agent's whole remaining budget.
|
|
4788
|
+
// The flow's budget: what is left after what has been spent AND
|
|
4789
|
+
// what every step and job running now holds — the parent's own
|
|
4790
|
+
// ceiling included. ceilingFor's equal share would hand the job a
|
|
4791
|
+
// second copy of an allowance its parent already holds.
|
|
4792
|
+
const held = [...reserved.values()].reduce((a, b) => a + b, 0);
|
|
4793
|
+
const flowLeft = run.budgetUsd && run.budgetUsd > 0 ? Math.max(0, run.budgetUsd - platform.runSpend(run) - held) : null;
|
|
4794
|
+
const ceiling = stepCeilingFor(flowLeft, agentBudgets.get(agent), run.steps
|
|
4795
|
+
.filter((s) => s.agent === agent)
|
|
4796
|
+
.reduce((sum, s) => sum + (s.costUsd ?? 0) + (s !== child && s.status === "running" ? (reserved.get(s) ?? 0) : 0), 0), agent, 1);
|
|
4797
|
+
if (ceiling.ceilingUsd === 0) {
|
|
4798
|
+
child.status = "failed";
|
|
4799
|
+
child.events.push({ t: new Date().toISOString(), type: "error", text: `over budget — nothing left to spend before this job (${ceiling.note}); it was not started` });
|
|
4800
|
+
entry.status = "failed";
|
|
4801
|
+
entry.ms = 0;
|
|
4802
|
+
save();
|
|
4803
|
+
return { status: "failed", result: `${agent} was not started: over budget (${ceiling.note})`, wrote: [] };
|
|
4804
|
+
}
|
|
4805
|
+
if (ceiling.ceilingUsd !== null)
|
|
4806
|
+
reserved.set(child, ceiling.ceilingUsd);
|
|
4807
|
+
save();
|
|
4808
|
+
// A running sandbox does not hear a stop flag: when the parent stops
|
|
4809
|
+
// waiting (its tool timed out, or its step ended), destroy the job's
|
|
4810
|
+
// sandbox, so nothing it does outlives the step that asked for it.
|
|
4811
|
+
const parentGone = () => run.stopRequested === true || cancelled() || (parent.status !== "running" && parent.status !== "pending");
|
|
4812
|
+
let killedIt = false;
|
|
4813
|
+
const watch = setInterval(() => {
|
|
4814
|
+
if (killedIt || !parentGone() || !child.sandbox?.ref)
|
|
4815
|
+
return;
|
|
4816
|
+
killedIt = true;
|
|
4817
|
+
child.events.push({ t: new Date().toISOString(), type: "error", text: "stopped — the step that asked for this job stopped waiting for it" });
|
|
4818
|
+
try {
|
|
4819
|
+
platform.killSandbox?.(child.sandbox);
|
|
4820
|
+
}
|
|
4821
|
+
catch {
|
|
4822
|
+
// its own deadline ends it
|
|
4823
|
+
}
|
|
4824
|
+
}, 2_000);
|
|
4825
|
+
watch.unref?.();
|
|
4826
|
+
try {
|
|
4827
|
+
await runStep(dir, tenant, child, null, save, modelOverride, tags, effortOverride, run.id, undefined, ceiling.ceilingUsd, ceiling.note, run.test === true, () => {
|
|
4828
|
+
save();
|
|
4829
|
+
// The parent's step ending or cancelling ends the job too —
|
|
4830
|
+
// here while it waits for room; the watch above once it runs.
|
|
4831
|
+
return parentGone();
|
|
4832
|
+
}, flowTimezone, at);
|
|
4833
|
+
}
|
|
4834
|
+
catch (err) {
|
|
4835
|
+
child.status = "failed";
|
|
4836
|
+
child.events.push({ t: new Date().toISOString(), type: "error", text: err instanceof Error ? err.message : String(err) });
|
|
4837
|
+
}
|
|
4838
|
+
finally {
|
|
4839
|
+
clearInterval(watch);
|
|
4840
|
+
reserved.delete(child);
|
|
4841
|
+
}
|
|
4842
|
+
// runStep mutates child.status; widen past TS's "running" narrowing.
|
|
4843
|
+
const ended = child.status;
|
|
4844
|
+
const status = ended === "completed" ? "completed" : ended === "skipped" ? "skipped" : "failed";
|
|
4845
|
+
entry.status = status;
|
|
4846
|
+
entry.costUsd = child.costUsd ?? null;
|
|
4847
|
+
entry.ms = Date.now() - started;
|
|
4848
|
+
save();
|
|
4849
|
+
const errors = child.events.filter((e) => e.type === "error").map((e) => e.text).join("\n");
|
|
4850
|
+
return {
|
|
4851
|
+
status,
|
|
4852
|
+
result: child.result ?? (errors || "(no reply)"),
|
|
4853
|
+
wrote: child.wrote ?? [],
|
|
4854
|
+
};
|
|
4855
|
+
};
|
|
4584
4856
|
// An agent's own `budget:` — the most it may spend in one run — is
|
|
4585
4857
|
// read at launch, so a cap raised mid-run applies to the next step.
|
|
4586
4858
|
const agentBudgets = new Map(listAgents(tenant, workspace).map((a) => [a.name, a.budget]));
|
|
@@ -4700,7 +4972,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4700
4972
|
() => {
|
|
4701
4973
|
save();
|
|
4702
4974
|
return run.stopRequested === true;
|
|
4703
|
-
}, flowTimezone, run.steps.indexOf(step), siblingsOf(step), peerBus).finally(() => reserved.delete(step));
|
|
4975
|
+
}, flowTimezone, run.steps.indexOf(step), siblingsOf(step), peerBus, delegateFor(step)).finally(() => reserved.delete(step));
|
|
4704
4976
|
// runStep mutates step.status; read it through a widened local
|
|
4705
4977
|
// so TS doesn't keep the "running" narrowing from above.
|
|
4706
4978
|
const outcome = step.status;
|
|
@@ -4826,8 +5098,10 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4826
5098
|
// answered an earlier failure — an evaluator loop's previous cycle
|
|
4827
5099
|
// — not this one.
|
|
4828
5100
|
const leftovers = [];
|
|
4829
|
-
for (let j = run.steps.indexOf(step) + 1; j < run.steps.length &&
|
|
5101
|
+
for (let j = run.steps.indexOf(step) + 1; j < run.steps.length && isSplicedRecord(run.steps[j]); j++) {
|
|
4830
5102
|
const r = run.steps[j];
|
|
5103
|
+
if (r.subagentOf)
|
|
5104
|
+
continue;
|
|
4831
5105
|
if (r.agent === step.onFail && (r.rescueOf === undefined || r.rescueOf === step.agent) && (r.status === "pending" || r.status === "running")) {
|
|
4832
5106
|
leftovers.push(r);
|
|
4833
5107
|
}
|
|
@@ -4935,7 +5209,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4935
5209
|
// A rescue record's own failure is already on the step it took over
|
|
4936
5210
|
// ("also failed"); counted here it would fail a group whose step a
|
|
4937
5211
|
// later rescue completed.
|
|
4938
|
-
const requiredFailed = freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !
|
|
5212
|
+
const requiredFailed = freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !isSplicedRecord(s));
|
|
4939
5213
|
if (requiredFailed) {
|
|
4940
5214
|
run.status = "failed";
|
|
4941
5215
|
break;
|
|
@@ -5014,7 +5288,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
5014
5288
|
// A rescue answered its own failure and stays as it ended: reset
|
|
5015
5289
|
// to pending it read as an interrupted rescue, and the next
|
|
5016
5290
|
// cycle's failure was handed the old one's instructions.
|
|
5017
|
-
if (
|
|
5291
|
+
if (isSplicedRecord(s))
|
|
5018
5292
|
continue;
|
|
5019
5293
|
s.status = "pending";
|
|
5020
5294
|
s.result = null;
|
|
@@ -5360,6 +5634,9 @@ export async function waitForRun(tenant, workspace, runId, timeoutMs = 5 * 60_00
|
|
|
5360
5634
|
*/
|
|
5361
5635
|
export function runResult(run) {
|
|
5362
5636
|
for (let i = run.steps.length - 1; i >= 0; i--) {
|
|
5637
|
+
// A sandbox: own job's reply went to the step that asked (runSummary).
|
|
5638
|
+
if (run.steps[i].subagentOf)
|
|
5639
|
+
continue;
|
|
5363
5640
|
const r = run.steps[i].result;
|
|
5364
5641
|
if (r && r.trim())
|
|
5365
5642
|
return r;
|
package/dist/src/scan.js
CHANGED
|
@@ -77,7 +77,7 @@ export const SCAN_RULES = [
|
|
|
77
77
|
title: "Reads untrusted content, holds private data and can send it out",
|
|
78
78
|
help: "An agent with all three can be told — by any page it reads — to send what it holds anywhere it can reach. " +
|
|
79
79
|
"Take one leg away: take it off the open network (`network: { level: trusted, allow: [the sites it needs] }` — a bare list only adds sites, " +
|
|
80
|
-
"and an agent that reads the web stays open), split the reading and the sending into two agents, " +
|
|
80
|
+
"and an agent that reads the web stays open), split the reading and the sending into two agents (a step each, or a reader sub-agent with \`sandbox: own\`), " +
|
|
81
81
|
"or guard its calls with a `hooks:` gate. An approval (`!`) on every outward step is the other answer.",
|
|
82
82
|
owasp: ["ASI01", "ASI02", "ASI03"],
|
|
83
83
|
},
|
|
@@ -90,6 +90,16 @@ export const SCAN_RULES = [
|
|
|
90
90
|
"and if that rule ever changed, they would be a privilege escalation. Grant them to the parent or remove them.",
|
|
91
91
|
owasp: ["ASI03"],
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: "security/subagent-own-sandbox",
|
|
95
|
+
category: "security",
|
|
96
|
+
level: "info",
|
|
97
|
+
title: "A sub-agent works in its own sandbox, with tools its parent does not have",
|
|
98
|
+
help: "`sandbox: own` on a sub-agent runs each job it is handed in a sandbox of its own, with its own tools, secrets and network — " +
|
|
99
|
+
"not cut to the parent's. That is the point (a reader with the web and no secrets, under a writer with secrets and no web), " +
|
|
100
|
+
"so this only says what it holds. Only its reply reaches the parent.",
|
|
101
|
+
owasp: ["ASI03"],
|
|
102
|
+
},
|
|
93
103
|
{
|
|
94
104
|
id: "supply-chain/pipe-to-shell",
|
|
95
105
|
category: "supply-chain",
|
|
@@ -650,6 +660,12 @@ function scanAgents(agents, outward, toolDefs, push, networks) {
|
|
|
650
660
|
if (!sub || s === a.name)
|
|
651
661
|
continue;
|
|
652
662
|
const extra = refNames(sub.data.tools).filter((t) => !tools.includes(t));
|
|
663
|
+
if (sub.data.sandbox === "own") {
|
|
664
|
+
if (extra.length) {
|
|
665
|
+
push("security/subagent-own-sandbox", a.file, keyLine(a.lines, "subagents"), `subagent ${s} own ${extra.join(",")}`, `subagents: [${s}] — ${s} runs in its own sandbox with ${extra.join(", ")}, which ${a.name} does not have; only its reply comes back`);
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
653
669
|
if (extra.length && tools.length) {
|
|
654
670
|
push("security/subagent-wider", a.file, keyLine(a.lines, "subagents"), `subagent ${s} ${extra.join(",")}`, `subagents: [${s}] — ${s} declares ${extra.join(", ")}, which ${a.name} does not have; inside the delegation they are dropped`);
|
|
655
671
|
}
|
package/dist/src/starter.js
CHANGED
|
@@ -478,6 +478,7 @@ apis: [{ name: crm }] # an HTTP API, as a tool
|
|
|
478
478
|
mcpServers: {} # an MCP server, as a tool
|
|
479
479
|
agents: [writer] # colleagues this one may consult (an answer, no tools)
|
|
480
480
|
subagents: [researcher] # colleagues it may delegate a job to (own context, own tools ≤ its own)
|
|
481
|
+
sandbox: own # delegated to: run each job in its own sandbox (own tools, secrets, network)
|
|
481
482
|
secrets: [CRM_TOKEN] # keys the agent uses itself; a tool's own come with the tool
|
|
482
483
|
limits: {web.search: 40, crm: 20, calls: 300} # per step; a call past a limit is refused, never run
|
|
483
484
|
network: [api.example.com] # sites its sandbox may reach, added to the trusted list; open | trusted | none
|
package/dist/src/store.d.ts
CHANGED
|
@@ -311,6 +311,8 @@ export interface AgentInfo {
|
|
|
311
311
|
/** Colleagues this agent may DELEGATE to (`subagents:`) — each runs with
|
|
312
312
|
* its own context and its own tools, no wider than this agent's. */
|
|
313
313
|
subagents: string[];
|
|
314
|
+
/** `sandbox: own` — delegated to, it runs each job in its own sandbox. */
|
|
315
|
+
sandbox?: "own";
|
|
314
316
|
/** The `skills:` allowlist, or null when the field is absent — which is
|
|
315
317
|
* not the same thing: absent inherits every skill in scope, an empty
|
|
316
318
|
* list withholds all of them. */
|
|
@@ -1068,6 +1070,16 @@ export interface StepAttempt {
|
|
|
1068
1070
|
export declare function sumAttempts(tries: StepAttempt[]): Pick<StepAttempt, "costUsd" | "tokens" | "computeSecs" | "turns">;
|
|
1069
1071
|
export { type TokenUsage, type PromptLayerRecord, tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens, promptLayersChanged, promptCacheNote } from "./tokens.ts";
|
|
1070
1072
|
import { type TokenUsage, type PromptLayerRecord } from "./tokens.ts";
|
|
1073
|
+
/** One job a step handed to an isolated sub-agent. `id` is the job's own
|
|
1074
|
+
* record's `delegation` — not an index, which moves when records are
|
|
1075
|
+
* spliced in before it. */
|
|
1076
|
+
export interface StepDelegation {
|
|
1077
|
+
id: string;
|
|
1078
|
+
agent: string;
|
|
1079
|
+
status: "running" | "completed" | "failed" | "skipped";
|
|
1080
|
+
costUsd?: number | null;
|
|
1081
|
+
ms?: number | null;
|
|
1082
|
+
}
|
|
1071
1083
|
/** The marker rescueStep puts in a rescue's instruction; how a rescue record
|
|
1072
1084
|
* written before `rescueOf` existed is still recognised. */
|
|
1073
1085
|
export declare const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
@@ -1076,6 +1088,15 @@ export declare function isRescueRecord(step: {
|
|
|
1076
1088
|
rescueOf?: string;
|
|
1077
1089
|
instruction?: string | null;
|
|
1078
1090
|
}): boolean;
|
|
1091
|
+
/** Is this record put in by the runner rather than by the flow — an on-fail
|
|
1092
|
+
* rescue, or an isolated sub-agent's run (`subagentOf`)? Neither is launched
|
|
1093
|
+
* with a group, handed on as a group's output, reset by a loop, or copied by
|
|
1094
|
+
* a re-run as work. */
|
|
1095
|
+
export declare function isSplicedRecord(step: {
|
|
1096
|
+
rescueOf?: string;
|
|
1097
|
+
subagentOf?: string;
|
|
1098
|
+
instruction?: string | null;
|
|
1099
|
+
}): boolean;
|
|
1079
1100
|
export interface StepRecord {
|
|
1080
1101
|
agent: string;
|
|
1081
1102
|
instruction: string;
|
|
@@ -1242,6 +1263,19 @@ export interface StepRecord {
|
|
|
1242
1263
|
* steps). Records written before this field are recognised by their
|
|
1243
1264
|
* instruction's `<rescue>` block instead — see isRescueRecord. */
|
|
1244
1265
|
rescueOf?: string;
|
|
1266
|
+
/** Set on an isolated sub-agent's run (`sandbox: own`, delegate-tools.ts):
|
|
1267
|
+
* the agent of the step that delegated to it. It ran in its own sandbox
|
|
1268
|
+
* inside that step; only its reply went back. Like a rescue it is not a
|
|
1269
|
+
* step of the flow — see isSplicedRecord. */
|
|
1270
|
+
subagentOf?: string;
|
|
1271
|
+
/** On an isolated sub-agent's run: its job's id, as the delegating step's
|
|
1272
|
+
* `delegations[].id` names it. */
|
|
1273
|
+
delegation?: string;
|
|
1274
|
+
/** On a step that delegated to isolated sub-agents: each job, in order. */
|
|
1275
|
+
delegations?: StepDelegation[];
|
|
1276
|
+
/** On an isolated sub-agent's run: the workspace paths its sandbox's
|
|
1277
|
+
* changes were applied to — what its delegating step is told it wrote. */
|
|
1278
|
+
wrote?: string[];
|
|
1245
1279
|
/**
|
|
1246
1280
|
* Sandbox seconds this step rented, and how many of them were cold start.
|
|
1247
1281
|
* Set only on the isolated path — an in-process step rents nothing, so
|
package/dist/src/store.js
CHANGED
|
@@ -1163,6 +1163,7 @@ export function listAgents(tenant, workspace) {
|
|
|
1163
1163
|
retiredTools: retiredToolNames(data),
|
|
1164
1164
|
consults: refNames(data.agents),
|
|
1165
1165
|
subagents: refNames(data.subagents),
|
|
1166
|
+
...(data.sandbox === "own" ? { sandbox: "own" } : {}),
|
|
1166
1167
|
skills: data.skills === undefined ? null : refNames(data.skills),
|
|
1167
1168
|
secrets: Array.isArray(data.secrets) ? data.secrets.map(String) : [],
|
|
1168
1169
|
budget: runBudget(data.budget),
|
|
@@ -2662,6 +2663,13 @@ export const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
|
2662
2663
|
export function isRescueRecord(step) {
|
|
2663
2664
|
return !!step.rescueOf || (step.instruction ?? "").includes(RESCUE_MARKER);
|
|
2664
2665
|
}
|
|
2666
|
+
/** Is this record put in by the runner rather than by the flow — an on-fail
|
|
2667
|
+
* rescue, or an isolated sub-agent's run (`subagentOf`)? Neither is launched
|
|
2668
|
+
* with a group, handed on as a group's output, reset by a loop, or copied by
|
|
2669
|
+
* a re-run as work. */
|
|
2670
|
+
export function isSplicedRecord(step) {
|
|
2671
|
+
return !!step.subagentOf || isRescueRecord(step);
|
|
2672
|
+
}
|
|
2665
2673
|
// ---------------------------------------------------------------- captured calls
|
|
2666
2674
|
//
|
|
2667
2675
|
// What each tool call took and gave (FOL-54 step 4), beside the run under
|
|
@@ -3218,6 +3226,10 @@ export function runVerdict(run) {
|
|
|
3218
3226
|
}
|
|
3219
3227
|
export function runSummary(run) {
|
|
3220
3228
|
for (let i = run.steps.length - 1; i >= 0; i--) {
|
|
3229
|
+
// A sandbox: own job is appended after every step of the flow; its reply
|
|
3230
|
+
// went to the step that asked, which speaks for the run.
|
|
3231
|
+
if (run.steps[i].subagentOf)
|
|
3232
|
+
continue;
|
|
3221
3233
|
const line = stepHeadline(run.steps[i]);
|
|
3222
3234
|
if (line)
|
|
3223
3235
|
return line;
|
package/dist/src/subagents.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export interface SubagentSpec {
|
|
|
10
10
|
* the step's own) or "inherit". */
|
|
11
11
|
model: string;
|
|
12
12
|
}
|
|
13
|
+
/** An isolated sub-agent (`sandbox: own` on its own agent.md): what the
|
|
14
|
+
* parent's sandbox needs to offer it as a tool. Everything else — its tools,
|
|
15
|
+
* secrets, network, size — is resolved on the worker when the job arrives,
|
|
16
|
+
* from its own file, and never enters the parent's sandbox. */
|
|
17
|
+
export interface DelegateSpec {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
/** Does this agent run in its own sandbox when delegated to? */
|
|
22
|
+
export declare function ownSandbox(front: {
|
|
23
|
+
sandbox?: unknown;
|
|
24
|
+
}): boolean;
|
|
25
|
+
/** The tool the parent calls to hand an isolated sub-agent a job. */
|
|
26
|
+
export declare function delegateToolName(agent: string): string;
|
|
13
27
|
/** The SDK's delegation tool, under both names it has had. Never granted to a
|
|
14
28
|
* sub-agent: that is what makes the depth one. */
|
|
15
29
|
export declare const DELEGATE_TOOLS: readonly ["Agent", "Task"];
|