@herbertgao/pi-subagents 0.18.5 → 0.22.0-local.1
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 +16 -0
- package/README.md +1 -1
- package/docs/packages.md +211 -0
- package/docs/rpc.md +61 -63
- package/docs/workflows.md +24 -16
- package/package.json +6 -6
- package/src/agent-file-toggle.ts +51 -14
- package/src/agent-manager.ts +123 -55
- package/src/agent-runner.ts +144 -13
- package/src/agent-source-badge.ts +78 -0
- package/src/completion-outbox.ts +122 -0
- package/src/custom-agents.ts +171 -79
- package/src/index.ts +598 -297
- package/src/invocation-config.ts +1 -1
- package/src/mention-clone.ts +48 -72
- package/src/nested-tools.ts +1 -1
- package/src/output-file.ts +15 -6
- package/src/package-resources.ts +724 -0
- package/src/schedule.ts +1 -1
- package/src/settings.ts +66 -0
- package/src/types.ts +26 -4
- package/src/ui/agent-widget.ts +2 -0
- package/src/ui/conversation-viewer.ts +354 -32
- package/src/ui/fleet-list.ts +33 -15
- package/src/workflow/collisions.ts +4 -1
- package/src/workflow/host.ts +6 -0
- package/src/workflow/json-schema.ts +9 -8
- package/src/workflow/saved.ts +48 -2
package/src/agent-file-toggle.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function findAgentFile(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Find the file behind a
|
|
64
|
+
* Find the *editable* file behind a loaded agent, preferring the path the loader
|
|
65
65
|
* actually read (`AgentConfig.sourcePath`) over the `<type>.md` guess.
|
|
66
66
|
*
|
|
67
67
|
* An agent's type comes from its frontmatter `name:` now, so the two can
|
|
@@ -73,13 +73,22 @@ export function findAgentFile(
|
|
|
73
73
|
*
|
|
74
74
|
* The probe stays as the fallback: a built-in that was never ejected has no
|
|
75
75
|
* `sourcePath`, and a path can go stale between a load and this call.
|
|
76
|
+
*
|
|
77
|
+
* **A package agent has no editable file.** Its `sourcePath` points inside pi's
|
|
78
|
+
* install root, where an edit is lost on the next `pi update` and a delete is
|
|
79
|
+
* undone by the next `pi install`. Passing `source` lets that rule live here
|
|
80
|
+
* rather than at each `/agents` call site: the package path is ignored, and the
|
|
81
|
+
* name probe runs instead — which finds the local stub shadowing it, if one
|
|
82
|
+
* exists, and otherwise nothing. Callers then take their no-file branch, which
|
|
83
|
+
* is the Eject-or-shadow behaviour a not-yet-ejected builtin already gets.
|
|
76
84
|
*/
|
|
77
85
|
export function locateAgentFile(
|
|
78
86
|
name: string,
|
|
79
87
|
sourcePath: string | undefined,
|
|
80
88
|
cwd: string = process.cwd(),
|
|
89
|
+
source?: AgentConfig["source"],
|
|
81
90
|
): { path: string; location: AgentFileLocation } | undefined {
|
|
82
|
-
if (sourcePath && existsSync(sourcePath)) {
|
|
91
|
+
if (source !== "package" && sourcePath && existsSync(sourcePath)) {
|
|
83
92
|
return { path: sourcePath, location: classifyAgentDir(sourcePath, cwd) }
|
|
84
93
|
}
|
|
85
94
|
return findAgentFile(name, cwd)
|
|
@@ -87,9 +96,11 @@ export function locateAgentFile(
|
|
|
87
96
|
|
|
88
97
|
/**
|
|
89
98
|
* Which discovery location a loaded agent's file came from. Only ever names
|
|
90
|
-
* a directory in a confirmation prompt, so an unrecognized parent
|
|
91
|
-
*
|
|
92
|
-
*
|
|
99
|
+
* a directory in a confirmation prompt, so an unrecognized parent reports as
|
|
100
|
+
* personal rather than widening the type for a case that has no better answer.
|
|
101
|
+
*
|
|
102
|
+
* Package-provided paths never reach here: `locateAgentFile` drops them above,
|
|
103
|
+
* because none of the three locations this classifies is where they live.
|
|
93
104
|
*/
|
|
94
105
|
function classifyAgentDir(path: string, cwd: string): AgentFileLocation {
|
|
95
106
|
if (path.startsWith(projectAgentsDir(cwd) + sep)) return "project"
|
|
@@ -251,11 +262,29 @@ ${input.systemPrompt}
|
|
|
251
262
|
`
|
|
252
263
|
}
|
|
253
264
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Rebuild the `tools:` CSV from the two halves the loader split it into.
|
|
267
|
+
*
|
|
268
|
+
* `extSelectors` has to come back with the built-ins: `parseToolsField` splits
|
|
269
|
+
* `tools: read, ext:mcp/github` into `["read"]` and `["ext:mcp/github"]`, so
|
|
270
|
+
* writing only the first half hands the ejected copy a *narrower* toolset than
|
|
271
|
+
* the file it came from. That was invisible while Eject only ever ran on
|
|
272
|
+
* built-in defaults, which carry no selectors; a package agent can.
|
|
273
|
+
*
|
|
274
|
+
* Zero built-ins with selectors present is not `none` — `none` means no tools at
|
|
275
|
+
* all, while `ext:` entries alone mean "extension tools only", which the loader
|
|
276
|
+
* reads back from a `tools:` line carrying just those entries.
|
|
277
|
+
*/
|
|
278
|
+
function formatToolsField(
|
|
279
|
+
tools: string[] | undefined,
|
|
280
|
+
extSelectors?: string[],
|
|
281
|
+
): string {
|
|
282
|
+
const selectors = extSelectors ?? []
|
|
283
|
+
if (tools === undefined)
|
|
284
|
+
return selectors.length > 0 ? ["*", ...selectors].join(", ") : "all"
|
|
285
|
+
if (tools.length === 0)
|
|
286
|
+
return selectors.length > 0 ? selectors.join(", ") : "none"
|
|
287
|
+
return [...tools, ...selectors].join(", ")
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
/** Serialize an AgentConfig to a full .md file (frontmatter + system prompt) for eject. */
|
|
@@ -267,7 +296,9 @@ export function serializeAgentFile(cfg: AgentConfig): string {
|
|
|
267
296
|
// Absent means "all built-ins"; an EMPTY list means explicitly zero. Writing
|
|
268
297
|
// `all` for both would hand a deliberately tool-less agent the whole toolbox
|
|
269
298
|
// the first time it is ejected.
|
|
270
|
-
fmFields.push(
|
|
299
|
+
fmFields.push(
|
|
300
|
+
`tools: ${formatToolsField(cfg.builtinToolNames, cfg.extSelectors)}`,
|
|
301
|
+
)
|
|
271
302
|
if (cfg.model) fmFields.push(`model: ${cfg.model}`)
|
|
272
303
|
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`)
|
|
273
304
|
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`)
|
|
@@ -290,15 +321,21 @@ export function serializeAgentFile(cfg: AgentConfig): string {
|
|
|
290
321
|
if (cfg.inheritContext) fmFields.push("inherit_context: true")
|
|
291
322
|
// Both cases, not just `true`: with `backgroundByDefault` on, omitting the
|
|
292
323
|
// field means background, so `false` is the only way to pin an agent file to
|
|
293
|
-
// foreground and is no longer interchangeable with absence.
|
|
294
|
-
//
|
|
295
|
-
// so this keeps the writer symmetric with the loader, nothing more.
|
|
324
|
+
// foreground and is no longer interchangeable with absence. Reachable since
|
|
325
|
+
// package agents became ejectable — a built-in default omits the field.
|
|
296
326
|
if (cfg.runInBackground !== undefined)
|
|
297
327
|
fmFields.push(`run_in_background: ${cfg.runInBackground}`)
|
|
298
328
|
if (cfg.outputTranscript === false) fmFields.push("output_transcript: false")
|
|
299
329
|
if (cfg.isolated) fmFields.push("isolated: true")
|
|
300
330
|
if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`)
|
|
301
331
|
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`)
|
|
332
|
+
// Also only reachable through a package agent: no built-in default sets
|
|
333
|
+
// either. Omitting them would have the ejected copy silently stop persisting
|
|
334
|
+
// its session, or write it somewhere else.
|
|
335
|
+
if (cfg.persistSession !== undefined)
|
|
336
|
+
fmFields.push(`persist_session: ${cfg.persistSession}`)
|
|
337
|
+
if (cfg.sessionDir)
|
|
338
|
+
fmFields.push(`session_dir: ${JSON.stringify(cfg.sessionDir)}`)
|
|
302
339
|
|
|
303
340
|
return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`
|
|
304
341
|
}
|
package/src/agent-manager.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ExtensionContext,
|
|
25
25
|
} from "@earendil-works/pi-coding-agent"
|
|
26
26
|
import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js"
|
|
27
|
+
import { getAgentConfig } from "./agent-types.js"
|
|
27
28
|
import { assignHandle, handleBase } from "./mention.js"
|
|
28
29
|
import { describeModel } from "./model-resolver.js"
|
|
29
30
|
import type {
|
|
@@ -32,6 +33,7 @@ import type {
|
|
|
32
33
|
AgentTombstone,
|
|
33
34
|
IsolationMode,
|
|
34
35
|
MentionResolution,
|
|
36
|
+
Pool,
|
|
35
37
|
SubagentType,
|
|
36
38
|
ThinkingLevel,
|
|
37
39
|
} from "./types.js"
|
|
@@ -191,9 +193,6 @@ function occupiesForegroundSlot(
|
|
|
191
193
|
return !!record.blocking && isTopLevelAgent(record)
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
/** Which concurrency pool a spawn is charged to, if any. */
|
|
195
|
-
type Pool = "background" | "foreground"
|
|
196
|
-
|
|
197
196
|
interface SpawnArgs {
|
|
198
197
|
pi: ExtensionAPI
|
|
199
198
|
ctx: ExtensionContext
|
|
@@ -658,10 +657,9 @@ export class AgentManager {
|
|
|
658
657
|
* an aborted signal, so a `spawnAndWait` on it would wait forever — pi has no
|
|
659
658
|
* tool-execution timeout to bail it out.
|
|
660
659
|
*
|
|
661
|
-
* The listener is
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* `abortAll`/`dispose` paths could not offer anyway.
|
|
660
|
+
* The listener is removable (`record.detachParentAbort`): `startAgent`
|
|
661
|
+
* replaces it with its own, and `sendToBackground` must be able to take it
|
|
662
|
+
* off, or the caller that armed it could stop a run it no longer owns.
|
|
665
663
|
*/
|
|
666
664
|
private armQueuedAbort(id: string, signal?: AbortSignal): boolean {
|
|
667
665
|
if (signal === undefined) return true
|
|
@@ -673,10 +671,39 @@ export class AgentManager {
|
|
|
673
671
|
}
|
|
674
672
|
return false
|
|
675
673
|
}
|
|
676
|
-
|
|
674
|
+
const record = this.agents.get(id)
|
|
675
|
+
if (record) this.armParentAbort(record, signal)
|
|
677
676
|
return true
|
|
678
677
|
}
|
|
679
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Stop `record` when its caller's `signal` aborts, replacing any listener an
|
|
681
|
+
* earlier stage armed — at most one is live per record, so one call to
|
|
682
|
+
* `detachParentAbort` cuts the run loose from its caller.
|
|
683
|
+
*/
|
|
684
|
+
private armParentAbort(
|
|
685
|
+
record: AgentRecord,
|
|
686
|
+
signal: AbortSignal | undefined,
|
|
687
|
+
): void {
|
|
688
|
+
this.detachParentAbort(record)
|
|
689
|
+
if (signal === undefined) return
|
|
690
|
+
const onParentAbort = () => this.abort(record.id)
|
|
691
|
+
signal.addEventListener("abort", onParentAbort, { once: true })
|
|
692
|
+
record.detachParentAbort = () =>
|
|
693
|
+
signal.removeEventListener("abort", onParentAbort)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private detachParentAbort(record: AgentRecord): void {
|
|
697
|
+
record.detachParentAbort?.()
|
|
698
|
+
record.detachParentAbort = undefined
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Add `delta` to the running count of `pool`; no pool, no count. */
|
|
702
|
+
private chargePool(pool: Pool | undefined, delta: 1 | -1): void {
|
|
703
|
+
if (pool === "background") this.runningBackground += delta
|
|
704
|
+
else if (pool === "foreground") this.runningForeground += delta
|
|
705
|
+
}
|
|
706
|
+
|
|
680
707
|
/**
|
|
681
708
|
* Kick off an agent's startup and register it under `startups`. The returned
|
|
682
709
|
* promise never rejects — the failure is delivered through `awaitStartup`,
|
|
@@ -762,23 +789,19 @@ export class AgentManager {
|
|
|
762
789
|
// still copying its repo. Claiming "running" here also keeps abort() and
|
|
763
790
|
// abortAll() able to reach an agent whose worktree is still being created.
|
|
764
791
|
//
|
|
765
|
-
// The pool is resolved ONCE, here, and
|
|
792
|
+
// The pool is resolved ONCE, here, and recorded for `settleRun`:
|
|
766
793
|
// `poolFor` reads `maxConcurrentForeground`, which the user can change from
|
|
767
794
|
// `/agents → Settings` mid-run, so recomputing it at settle time would
|
|
768
795
|
// decrement a pool this run never charged (counter underflow, limit
|
|
769
796
|
// silently lifted) or skip the decrement for one it did (leaked slot —
|
|
770
|
-
// every later blocking spawn queues forever).
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (pool === "background") this.runningBackground--
|
|
775
|
-
else if (pool === "foreground") this.runningForeground--
|
|
776
|
-
}
|
|
797
|
+
// every later blocking spawn queues forever). Only `sendToBackground`
|
|
798
|
+
// changes it afterwards, moving the counters with it. The two startup exits
|
|
799
|
+
// below never reach `settleRun`, so they hand the slot back themselves.
|
|
800
|
+
record.chargedPool = this.poolFor(record)
|
|
777
801
|
record.status = "running"
|
|
778
802
|
record.startedAt = Date.now()
|
|
779
803
|
record.startGate = undefined
|
|
780
|
-
|
|
781
|
-
else if (pool === "foreground") this.runningForeground++
|
|
804
|
+
this.chargePool(record.chargedPool, 1)
|
|
782
805
|
|
|
783
806
|
// Worktree isolation: try to create a temporary git worktree. Strict —
|
|
784
807
|
// fail loud if not possible (no silent fallback to main tree). Done BEFORE
|
|
@@ -786,11 +809,19 @@ export class AgentManager {
|
|
|
786
809
|
// The project switch is enforced here as well as at the tool boundary
|
|
787
810
|
// because cross-extension RPC forwards its options unvalidated — a schema
|
|
788
811
|
// that omits the field can't stop a caller that never saw the schema.
|
|
812
|
+
// The agent file's `isolation:` is resolved here too, not by each caller:
|
|
813
|
+
// mentions pass none, and RPC forwards only its caller's value. Workflow
|
|
814
|
+
// children are the exception, as in Claude Code, whose workflow agent()
|
|
815
|
+
// reads only the script's option and ignores the definition's.
|
|
816
|
+
const isolation =
|
|
817
|
+
options.workflowId !== undefined
|
|
818
|
+
? options.isolation
|
|
819
|
+
: (getAgentConfig(type)?.isolation ?? options.isolation)
|
|
789
820
|
let worktreeCwd: string | undefined
|
|
790
|
-
if (
|
|
821
|
+
if (isolation === "worktree" && isWorktreeIsolationEnabled()) {
|
|
791
822
|
const wt = await createWorktree(pi, baseCwd, id)
|
|
792
823
|
if (!wt) {
|
|
793
|
-
|
|
824
|
+
this.chargePool(record.chargedPool, -1)
|
|
794
825
|
throw new Error(
|
|
795
826
|
'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
796
827
|
"Initialize git and commit at least once, or omit `isolation`.",
|
|
@@ -805,6 +836,10 @@ export class AgentManager {
|
|
|
805
836
|
// subdirectory, silently dropping extensions/skills.
|
|
806
837
|
worktreeCwd = customCwd !== undefined ? wt.workPath : wt.path
|
|
807
838
|
this.worktreeRepos.add(baseCwd)
|
|
839
|
+
// The widget's "worktree" tag reads this; only callers that resolve the
|
|
840
|
+
// agent file themselves seed it.
|
|
841
|
+
record.invocation ??= {}
|
|
842
|
+
record.invocation.isolation = "worktree"
|
|
808
843
|
|
|
809
844
|
// No longer "running" means a stop landed while the copy was being made
|
|
810
845
|
// (abort(), abortAll()) — a window that did not exist when creation was
|
|
@@ -812,7 +847,7 @@ export class AgentManager {
|
|
|
812
847
|
// burn tokens on work nobody is waiting for: discard the fresh (and by
|
|
813
848
|
// definition unchanged) worktree instead.
|
|
814
849
|
if (record.status !== "running") {
|
|
815
|
-
|
|
850
|
+
this.chargePool(record.chargedPool, -1)
|
|
816
851
|
record.worktreeResult = await cleanupWorktree(
|
|
817
852
|
pi,
|
|
818
853
|
baseCwd,
|
|
@@ -829,24 +864,13 @@ export class AgentManager {
|
|
|
829
864
|
|
|
830
865
|
this.onStart?.(record)
|
|
831
866
|
|
|
832
|
-
// Wire parent abort signal to stop the subagent when the parent is interrupted
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
else {
|
|
840
|
-
const onParentAbort = () => this.abort(id)
|
|
841
|
-
options.signal.addEventListener("abort", onParentAbort, { once: true })
|
|
842
|
-
detachParentSignal = () =>
|
|
843
|
-
options.signal!.removeEventListener("abort", onParentAbort)
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
const detach = () => {
|
|
847
|
-
detachParentSignal?.()
|
|
848
|
-
detachParentSignal = undefined
|
|
849
|
-
}
|
|
867
|
+
// Wire parent abort signal to stop the subagent when the parent is interrupted.
|
|
868
|
+
// A queued spawn can start minutes after the caller handed us its signal,
|
|
869
|
+
// by which time it may already be aborted — and `addEventListener` would
|
|
870
|
+
// never fire, leaving a child the parent can no longer reach.
|
|
871
|
+
if (options.signal?.aborted) this.abort(id)
|
|
872
|
+
else this.armParentAbort(record, options.signal)
|
|
873
|
+
const detach = () => this.detachParentAbort(record)
|
|
850
874
|
|
|
851
875
|
const promise = runAgent(ctx, type, prompt, {
|
|
852
876
|
pi,
|
|
@@ -1019,7 +1043,7 @@ export class AgentManager {
|
|
|
1019
1043
|
|
|
1020
1044
|
this.abortOwnedChildren(id)
|
|
1021
1045
|
|
|
1022
|
-
this.settleRun(record, true
|
|
1046
|
+
this.settleRun(record, true)
|
|
1023
1047
|
return responseText
|
|
1024
1048
|
},
|
|
1025
1049
|
)
|
|
@@ -1061,7 +1085,7 @@ export class AgentManager {
|
|
|
1061
1085
|
|
|
1062
1086
|
this.abortOwnedChildren(id)
|
|
1063
1087
|
|
|
1064
|
-
this.settleRun(record, false
|
|
1088
|
+
this.settleRun(record, false)
|
|
1065
1089
|
return ""
|
|
1066
1090
|
})
|
|
1067
1091
|
|
|
@@ -1089,18 +1113,19 @@ export class AgentManager {
|
|
|
1089
1113
|
*
|
|
1090
1114
|
* @param guardCallback swallow a throwing `onComplete` (the success path does;
|
|
1091
1115
|
* the error path historically did not, and keeps not doing so).
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1116
|
+
*
|
|
1117
|
+
* Releases `record.chargedPool` — the pool the run was charged to at start
|
|
1118
|
+
* (or moved to by `sendToBackground`), never a recomputed one, so a mid-run
|
|
1119
|
+
* change to `maxConcurrentForeground` can't make the release disagree with
|
|
1120
|
+
* the acquire.
|
|
1095
1121
|
*/
|
|
1096
|
-
private settleRun(
|
|
1097
|
-
record: AgentRecord,
|
|
1098
|
-
guardCallback: boolean,
|
|
1099
|
-
pool: Pool | undefined,
|
|
1100
|
-
): void {
|
|
1122
|
+
private settleRun(record: AgentRecord, guardCallback: boolean): void {
|
|
1101
1123
|
if (!record.isBackground) record.resultConsumed = true
|
|
1102
|
-
|
|
1103
|
-
|
|
1124
|
+
// Nobody waits on a settled run; a later foreground resume must not look
|
|
1125
|
+
// like a spawn `sendToBackground` could hand off.
|
|
1126
|
+
record.blocking = false
|
|
1127
|
+
const pool = record.chargedPool
|
|
1128
|
+
this.chargePool(pool, -1)
|
|
1104
1129
|
|
|
1105
1130
|
if (guardCallback) {
|
|
1106
1131
|
try {
|
|
@@ -1222,10 +1247,10 @@ export class AgentManager {
|
|
|
1222
1247
|
// The run promise only exists once startup is past its awaited repo copy —
|
|
1223
1248
|
// without this the call would return before the agent had started at all.
|
|
1224
1249
|
// A startup failure (strict worktree isolation) rejects here, which is what
|
|
1225
|
-
// the immediate path owes its caller:
|
|
1226
|
-
//
|
|
1227
|
-
// instead (nobody was awaiting `startups` at drain time) and is
|
|
1228
|
-
// below, so the contract is the same either way.
|
|
1250
|
+
// the immediate path owes its caller: the Agent tool reports failure by
|
|
1251
|
+
// letting this throw out of `execute`. A queued spawn's failure landed on
|
|
1252
|
+
// the record instead (nobody was awaiting `startups` at drain time) and is
|
|
1253
|
+
// rethrown below, so the contract is the same either way.
|
|
1229
1254
|
await this.awaitStartup(id)
|
|
1230
1255
|
|
|
1231
1256
|
// undefined when it was aborted while queued, or stopped mid-copy, and so
|
|
@@ -1243,6 +1268,49 @@ export class AgentManager {
|
|
|
1243
1268
|
return { id, record }
|
|
1244
1269
|
}
|
|
1245
1270
|
|
|
1271
|
+
/**
|
|
1272
|
+
* Hand a live run to the background without restarting it. The session,
|
|
1273
|
+
* promise, in-flight turn, transcript and worktree all continue under the
|
|
1274
|
+
* same id; what changes is who answers for it. The caller's parent-abort
|
|
1275
|
+
* listener comes off (its Esc no longer reaches the run — `abort()` and
|
|
1276
|
+
* `/agents` still do), the slot moves to the background pool, and the run
|
|
1277
|
+
* then settles through the ordinary background path, since `settleRun`
|
|
1278
|
+
* reads the live `isBackground` and `chargedPool`.
|
|
1279
|
+
*
|
|
1280
|
+
* Releasing the caller is the caller's job — the `Agent` tool races its own
|
|
1281
|
+
* wake-up against `spawnAndWait`. This only refuses what it can see: a run
|
|
1282
|
+
* nobody is blocking on (a background or detached spawn, one already handed
|
|
1283
|
+
* off, or a foreground RESUME — `blocking` is cleared when the spawned run
|
|
1284
|
+
* settles, and a resume never charged the slot this would move), and one
|
|
1285
|
+
* that reports through something other than the session (a nested child, a
|
|
1286
|
+
* workflow's agent).
|
|
1287
|
+
*
|
|
1288
|
+
* The background slot is taken even when the pool is full: suspending a
|
|
1289
|
+
* live run is not an option, so it runs over the limit and queued
|
|
1290
|
+
* background work waits for the count to drop, as with `bypassQueue`.
|
|
1291
|
+
*/
|
|
1292
|
+
sendToBackground(id: string): boolean {
|
|
1293
|
+
const record = this.agents.get(id)
|
|
1294
|
+
if (
|
|
1295
|
+
!record?.blocking ||
|
|
1296
|
+
!isTopLevelAgent(record) ||
|
|
1297
|
+
record.status !== "running" ||
|
|
1298
|
+
!record.promise
|
|
1299
|
+
)
|
|
1300
|
+
return false
|
|
1301
|
+
|
|
1302
|
+
this.detachParentAbort(record)
|
|
1303
|
+
this.chargePool(record.chargedPool, -1)
|
|
1304
|
+
record.chargedPool = "background"
|
|
1305
|
+
this.chargePool(record.chargedPool, 1)
|
|
1306
|
+
record.blocking = false
|
|
1307
|
+
record.isBackground = true
|
|
1308
|
+
record.resultConsumed = false
|
|
1309
|
+
// A freed foreground slot may have unblocked a queued blocking spawn.
|
|
1310
|
+
this.drainQueue()
|
|
1311
|
+
return true
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1246
1314
|
/**
|
|
1247
1315
|
* Resume an existing agent session with a new prompt.
|
|
1248
1316
|
*/
|