pi-squire 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +323 -1
- package/README.md +918 -117
- package/package.json +2 -1
- package/src/agents.ts +57 -36
- package/src/child-tasks.ts +221 -0
- package/src/child.ts +35 -41
- package/src/completion-holds.ts +104 -0
- package/src/dashboard.ts +144 -0
- package/src/git.ts +6 -1
- package/src/identity.ts +152 -0
- package/src/index.ts +358 -56
- package/src/local-state.ts +21 -0
- package/src/navigator-view.ts +186 -0
- package/src/navigator.ts +152 -107
- package/src/poll.ts +1 -0
- package/src/render.ts +4 -1
- package/src/result-close.ts +37 -0
- package/src/result-restore.ts +95 -0
- package/src/result-types.ts +29 -1
- package/src/results-presentation.ts +47 -0
- package/src/results.ts +174 -108
- package/src/resume-checks.ts +14 -2
- package/src/resume-picker.ts +9 -2
- package/src/resume.ts +62 -13
- package/src/session-owner.ts +279 -0
- package/src/task-replies.ts +247 -0
- package/src/task-settlement.ts +81 -0
- package/src/task-store.ts +26 -0
- package/src/tmux.ts +134 -54
- package/src/tool-ceiling.ts +125 -0
- package/src/tracking.ts +72 -65
- package/src/tree-close.ts +236 -0
- package/src/tree-fence.ts +51 -0
- package/src/tree-lock.ts +77 -0
- package/src/tree-notice.ts +124 -0
- package/src/tree-registry.ts +365 -0
- package/src/tree-settings.ts +31 -0
- package/src/tree-types.ts +32 -0
- package/src/tree-view.ts +246 -0
- package/src/trust-control.ts +214 -0
- package/src/trust-prompt.ts +163 -0
- package/src/trust-wire.ts +203 -0
- package/src/widget-activity-join.ts +24 -0
- package/src/widget-activity-runtime.ts +104 -0
- package/src/widget-activity.ts +104 -0
- package/src/widget-ancestors.ts +142 -0
- package/src/widget-current.ts +41 -0
- package/src/widget-data.ts +147 -0
- package/src/widget-focus.ts +133 -0
- package/src/widget-live-process.ts +31 -0
- package/src/widget-live.ts +356 -0
- package/src/widget-render.ts +181 -0
- package/src/widget-selection.ts +58 -0
- package/src/widget-settings.ts +29 -0
- package/src/widget-summary.ts +51 -0
- package/src/widget-title-file.ts +127 -0
- package/src/widget-title.ts +118 -0
- package/src/widget.ts +272 -0
- package/src/window-placement.ts +30 -5
- package/src/workspace.ts +38 -90
package/src/index.ts
CHANGED
|
@@ -15,15 +15,13 @@
|
|
|
15
15
|
// in the parent session; its answer is taken over on `message:inbound` and
|
|
16
16
|
// shown as one result message, so the parent hears back on its own.
|
|
17
17
|
//
|
|
18
|
-
// With auto-exit on (the agent's `
|
|
18
|
+
// With auto-exit on (the agent's `auto_exit`, default true, overridden per
|
|
19
19
|
// call) the delegate closes its own window after a normal completion; the
|
|
20
20
|
// delegate's side lives in `child.ts`. With it off the window stays open.
|
|
21
21
|
// An agent's `tools`, or the parent's active tools when it has no `tools`
|
|
22
22
|
// field (less any `-name` entries), goes to the child as `--tools` with the
|
|
23
|
-
// `delegateTools` setting added (`--no-tools` when that leaves nothing)
|
|
24
|
-
//
|
|
25
|
-
// agent's own extensions register is passed only when `tools` or
|
|
26
|
-
// `delegateTools` names it. An agent with no `model` or `thinking` runs
|
|
23
|
+
// `delegateTools` setting added (`--no-tools` when that leaves nothing).
|
|
24
|
+
// An agent with no `model` or `thinking` runs
|
|
27
25
|
// on the parent's.
|
|
28
26
|
//
|
|
29
27
|
// The model gets four tools. `delegate` takes agent, task and optional model
|
|
@@ -36,9 +34,9 @@
|
|
|
36
34
|
// envelope paths.
|
|
37
35
|
// `delegation_close` kills the window and records the close. `delegation_resume`
|
|
38
36
|
// (`resume.ts`) reopens an exited delegate's session in a new window with a
|
|
39
|
-
// follow-up task, recorded as a new delegation.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
37
|
+
// follow-up task, recorded as a new delegation. A delegate can launch children
|
|
38
|
+
// only below maxDepth and with its current agent allow_nested_delegation grant.
|
|
39
|
+
// Durable identity restores that boundary even on a manual session reopening.
|
|
42
40
|
//
|
|
43
41
|
// The `session:launch` contract this package provides, and the `message:*`
|
|
44
42
|
// hooks it consumes, live in this package's README.
|
|
@@ -68,17 +66,41 @@ import {
|
|
|
68
66
|
withoutLocked,
|
|
69
67
|
} from "./agents.ts";
|
|
70
68
|
import { AGENT_ENV, AUTO_EXIT_ENV, FALLBACK_ENV, registerChild } from "./child.ts";
|
|
69
|
+
import { registerCompletionHost } from "./completion-holds.ts";
|
|
70
|
+
import { createIdentity, grantsNesting, type Identity, readIdentity, writeIdentity } from "./identity.ts";
|
|
71
71
|
import { activeMark, clearMarks, isFree, readMarks } from "./limits.ts";
|
|
72
72
|
import { createNavigator, OPEN_USAGE, openCompletions, parseOpen } from "./navigator.ts";
|
|
73
73
|
import { type NewDelegateDeps, type NewRequest, startNewDelegate } from "./new-delegate.ts";
|
|
74
74
|
import { dirtyNotice, renderDelegateCall, renderDelegateResult, resourceLine } from "./render.ts";
|
|
75
|
-
import { createResults, delegateName, type WorkspaceAttempt } from "./results.ts";
|
|
75
|
+
import { createResults, delegateName, isStarted, sessionIdOf, type WorkspaceAttempt } from "./results.ts";
|
|
76
76
|
import { createResume, type ResumeRequest, resumableIds, startedLine } from "./resume.ts";
|
|
77
77
|
import { pickResume } from "./resume-picker.ts";
|
|
78
|
+
import { ownedAPI, SessionOwner, sessionIsOwned } from "./session-owner.ts";
|
|
79
|
+
import { registerTaskReplies } from "./task-replies.ts";
|
|
78
80
|
import { createTmuxClient, type OpenedWindow, requireTmuxVersion, type TmuxClient, WindowOpenError } from "./tmux.ts";
|
|
81
|
+
import { availableTools, enforceToolCeiling } from "./tool-ceiling.ts";
|
|
79
82
|
import { createTracking, toolResult } from "./tracking.ts";
|
|
83
|
+
import { treeNotice } from "./tree-notice.ts";
|
|
84
|
+
import {
|
|
85
|
+
assertParentPlacement,
|
|
86
|
+
bindTree,
|
|
87
|
+
claimTree,
|
|
88
|
+
dispatchTree,
|
|
89
|
+
failedTree,
|
|
90
|
+
finishTree,
|
|
91
|
+
openedTree,
|
|
92
|
+
publishTreeRecords,
|
|
93
|
+
type Reservation,
|
|
94
|
+
readTree,
|
|
95
|
+
reconcileTree,
|
|
96
|
+
reserveTree,
|
|
97
|
+
} from "./tree-registry.ts";
|
|
98
|
+
import { assertResumeAuthority, historicalRecord, visibleRecords } from "./tree-view.ts";
|
|
99
|
+
import { registerTrustControl } from "./trust-control.ts";
|
|
80
100
|
import { createProactive, createUsageClient, type Proactive, type UsageClient } from "./usage.ts";
|
|
81
|
-
import {
|
|
101
|
+
import { registerDelegateWidget } from "./widget.ts";
|
|
102
|
+
import { registerActivityRuntime } from "./widget-activity-runtime.ts";
|
|
103
|
+
import { assertRootPlacement, placementProject, type WindowPlacement, windowPlacement } from "./window-placement.ts";
|
|
82
104
|
import {
|
|
83
105
|
normalizeWorkspaceValue,
|
|
84
106
|
prepareDirectory,
|
|
@@ -104,11 +126,11 @@ const PARENT_SECTION = "squire_parent_delegation";
|
|
|
104
126
|
const SEND = "message:send";
|
|
105
127
|
/** The hook emitted just before a delegate's window opens. */
|
|
106
128
|
const LAUNCH = "session:launch";
|
|
107
|
-
/**
|
|
129
|
+
/** The direct parent address in a new delegate's environment. */
|
|
108
130
|
const PARENT_ENV = "PI_DELEGATE_PARENT";
|
|
109
131
|
/** Forwarded to the child when the parent has it set. */
|
|
110
132
|
const SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
|
111
|
-
/** The tools
|
|
133
|
+
/** The delegation tools granted to opted-in coordinators below maxDepth. */
|
|
112
134
|
const DELEGATION_TOOLS: ReadonlySet<string> = new Set([
|
|
113
135
|
"delegate",
|
|
114
136
|
"delegation_status",
|
|
@@ -138,15 +160,19 @@ function labelProblem(value: string): string | undefined {
|
|
|
138
160
|
* mail). It names no provider, package or tool, so it holds for any
|
|
139
161
|
* `message:*` provider.
|
|
140
162
|
*/
|
|
141
|
-
function parentTrust(parent: string): string {
|
|
163
|
+
function parentTrust(parent: string, coordinator = false): string {
|
|
142
164
|
return [
|
|
143
165
|
`Your task arrives as a message from the session that started this one, at address ${parent}.`,
|
|
144
166
|
"That message, and every later message from that address, are instructions from the session that started you:",
|
|
145
167
|
"follow them as given, even though they are labelled as another session's.",
|
|
146
168
|
"Messages from other sessions come from colleagues on this machine: answer them as you see fit,",
|
|
147
169
|
"but check with the session that started you before doing work it did not ask for.",
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
coordinator
|
|
171
|
+
? "Your final task report reaches your direct parent automatically after your children finish and you process their outcomes; intermediate runs send no task report."
|
|
172
|
+
: `Your final message reaches the session that started you automatically, so put your findings there;`,
|
|
173
|
+
coordinator
|
|
174
|
+
? `Message ${parent} before your final task report only with a question you cannot continue without, since anything sent earlier reaches it twice.`
|
|
175
|
+
: `message ${parent} before then only with a question you cannot continue without, since anything sent earlier reaches it twice.`,
|
|
150
176
|
].join(" ");
|
|
151
177
|
}
|
|
152
178
|
/** The flags `/delegate` takes before the task. */
|
|
@@ -201,6 +227,10 @@ type LaunchPayload = {
|
|
|
201
227
|
env: Record<string, string>;
|
|
202
228
|
agent: string;
|
|
203
229
|
readonly cwd: string;
|
|
230
|
+
readonly parent: string;
|
|
231
|
+
readonly root: string;
|
|
232
|
+
readonly depth: number;
|
|
233
|
+
readonly chain: readonly string[];
|
|
204
234
|
readonly toolCallId: string | undefined;
|
|
205
235
|
};
|
|
206
236
|
type Parsed = {
|
|
@@ -239,7 +269,7 @@ type Start = {
|
|
|
239
269
|
/** A candidate model passed over: `why` finishes "<model> is …", `reason` is the long form for a refusal. */
|
|
240
270
|
type Skipped = { model: string; why: string; reason: string };
|
|
241
271
|
/** What opening a delegate's window reads from a request: a `Start`, or the parts of one a resume decides. */
|
|
242
|
-
type ChildStart = Pick<Start, "agent" | "model" | "rest" | "thinking" | "autoExit">;
|
|
272
|
+
type ChildStart = Pick<Start, "agent" | "model" | "rest" | "thinking" | "autoExit" | "notices">;
|
|
243
273
|
/** A delegation that is open in a tmux window. */
|
|
244
274
|
type Launched = {
|
|
245
275
|
id: string;
|
|
@@ -558,7 +588,7 @@ function delegateVisible(options: { selectedTools: string[]; hiddenTools: string
|
|
|
558
588
|
|
|
559
589
|
/** Where agent files go, for the messages that say how to add one. */
|
|
560
590
|
function whereAgentsGo(): string {
|
|
561
|
-
return `${join(globalAgentsDir(getAgentDir()), "<name>.md")} (or .pi/agents/<name>.md in a trusted project), with the delegate's instructions as its body and optional frontmatter: description, model, thinking, tools,
|
|
591
|
+
return `${join(globalAgentsDir(getAgentDir()), "<name>.md")} (or .pi/agents/<name>.md in a trusted project), with the delegate's instructions as its body and optional frontmatter: description, model, thinking, tools, fallback, auto_exit, prompt_mode, display_name, locked and enabled`;
|
|
562
592
|
}
|
|
563
593
|
|
|
564
594
|
/** One roster line for the `delegate` tool's description. */
|
|
@@ -623,8 +653,11 @@ function toolSelection(agent: Agent, parentTools: readonly string[], extra: read
|
|
|
623
653
|
}
|
|
624
654
|
|
|
625
655
|
/** The delegate's appended prompt: the agent's body, then the lines every delegate gets. */
|
|
626
|
-
function delegatePrompt(agent: Agent, parent: string, managedContext: string): string {
|
|
627
|
-
const
|
|
656
|
+
function delegatePrompt(agent: Agent, parent: string, managedContext: string, coordinator: boolean): string {
|
|
657
|
+
const final = coordinator
|
|
658
|
+
? "End the task with one self-contained final report to your direct parent after your child tasks finish and you process their outcomes."
|
|
659
|
+
: FINAL_LINE;
|
|
660
|
+
const tail = `${final}\n\n${parentTrust(parent, coordinator)}${managedContext}`;
|
|
628
661
|
if (agent.prompt === "") return tail;
|
|
629
662
|
// `replace` is how every delegate was built before `prompt_mode`, byte for byte.
|
|
630
663
|
const body =
|
|
@@ -659,21 +692,104 @@ function toolDescription(agents: readonly Agent[]): string {
|
|
|
659
692
|
* passes only `pi`, so they are not part of the package's documented contract.
|
|
660
693
|
*/
|
|
661
694
|
export default function delegate(
|
|
662
|
-
|
|
695
|
+
api: ExtensionAPI,
|
|
663
696
|
tmux: TmuxClient = createTmuxClient(),
|
|
664
697
|
usage: UsageClient = createUsageClient(),
|
|
665
698
|
): void {
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const results = createResults(
|
|
699
|
+
const owner = new SessionOwner();
|
|
700
|
+
const host = api;
|
|
701
|
+
host.on("session_start", (_event, ctx) => owner.start(ctx));
|
|
702
|
+
host.events.on("message:inbound", (payload) => owner.receive(payload as import("./results.ts").Inbound));
|
|
703
|
+
const pi = ownedAPI(host, owner);
|
|
704
|
+
const externalReady = registerCompletionHost(pi);
|
|
705
|
+
const identity = createIdentity(pi);
|
|
706
|
+
const results = createResults(
|
|
707
|
+
pi,
|
|
708
|
+
() => identity.current() !== undefined,
|
|
709
|
+
(ctx, records) =>
|
|
710
|
+
publishTreeRecords(
|
|
711
|
+
identity.current()?.root ?? ctx.sessionManager.getSessionId(),
|
|
712
|
+
ctx.sessionManager.getSessionId(),
|
|
713
|
+
records,
|
|
714
|
+
),
|
|
715
|
+
() => owner.active(),
|
|
716
|
+
(ctx) =>
|
|
717
|
+
readTree(identity.current()?.root ?? ctx.sessionManager.getSessionId())
|
|
718
|
+
.entries.filter((entry) => entry.identity.parent === ctx.sessionManager.getSessionId() && entry.closed)
|
|
719
|
+
.map((entry) => entry.id),
|
|
720
|
+
);
|
|
721
|
+
pi.on("session_start", (_event, ctx) => tracking.restore(ctx));
|
|
722
|
+
const replies = registerTaskReplies(
|
|
723
|
+
pi,
|
|
724
|
+
() => identity.current()?.parent,
|
|
725
|
+
() => {
|
|
726
|
+
try {
|
|
727
|
+
owner.assert();
|
|
728
|
+
identity.current();
|
|
729
|
+
return results.completionReady() && externalReady();
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
(ctx) => coordinates(currentRoster(ctx)),
|
|
735
|
+
(ctx, ids) => {
|
|
736
|
+
owner.assert();
|
|
737
|
+
const own = identity.current();
|
|
738
|
+
if (own) finishTree(own.root, ctx.sessionManager.getSessionId(), ids);
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
registerChild(pi, replies, identity.current, () => owner.active());
|
|
742
|
+
host.on("session_start", () => owner.arm());
|
|
743
|
+
// Restore answers synchronously before this potentially slow tmux read.
|
|
744
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
745
|
+
try {
|
|
746
|
+
const own = identity.current();
|
|
747
|
+
if (!own) return;
|
|
748
|
+
const window = await tmux.currentWindow?.();
|
|
749
|
+
owner.assert();
|
|
750
|
+
claimTree(own.root, ctx.sessionManager.getSessionId(), window);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
ctx.ui.notify(
|
|
753
|
+
`delegate: could not publish startup accounting: ${String(error)}; uncertain reservations remain charged.`,
|
|
754
|
+
"error",
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
674
758
|
|
|
675
759
|
const proactive = createProactive(usage);
|
|
676
|
-
|
|
760
|
+
let viewContext: ExtensionContext | undefined;
|
|
761
|
+
pi.on("session_start", (_event, ctx) => {
|
|
762
|
+
viewContext = ctx;
|
|
763
|
+
});
|
|
764
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
765
|
+
if (!identity.current() && results.list().length === 0) {
|
|
766
|
+
try {
|
|
767
|
+
await treeNotice(ctx, tmux, () => owner.active());
|
|
768
|
+
} catch (error) {
|
|
769
|
+
ctx.ui.notify(`delegate: owner discovery unavailable: ${String(error)}`, "warning");
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
const viewResults = {
|
|
774
|
+
...results,
|
|
775
|
+
list: () => (viewContext ? visibleRecords(results, viewContext) : results.list()),
|
|
776
|
+
find: (id: string) =>
|
|
777
|
+
viewContext ? visibleRecords(results, viewContext).find((record) => record.id === id) : results.find(id),
|
|
778
|
+
};
|
|
779
|
+
const tracking = createTracking(pi, results, tmux, proactive.errors, viewResults, () => owner.assert());
|
|
780
|
+
const trustControl = registerTrustControl(pi, identity.current);
|
|
781
|
+
// Factual lifecycle observation survives failed action processing; only creation needs ownership.
|
|
782
|
+
registerActivityRuntime(host, () => owner.active(), tmux, owner.token);
|
|
783
|
+
registerDelegateWidget(
|
|
784
|
+
pi,
|
|
785
|
+
results,
|
|
786
|
+
tmux,
|
|
787
|
+
async (ctx, sessionId) => {
|
|
788
|
+
owner.assert();
|
|
789
|
+
await navigator.panel(ctx, sessionId);
|
|
790
|
+
},
|
|
791
|
+
() => owner.assert(),
|
|
792
|
+
);
|
|
677
793
|
|
|
678
794
|
/** Captured because `getArgumentCompletions` is called without a context. */
|
|
679
795
|
let registry: ModelRegistry | undefined;
|
|
@@ -710,14 +826,37 @@ export default function delegate(
|
|
|
710
826
|
|
|
711
827
|
/**
|
|
712
828
|
* The delegate's tools for a launch or resume starting now: the agent's
|
|
713
|
-
* named tools, or the parent's
|
|
714
|
-
* `-name` entries), then
|
|
715
|
-
*
|
|
716
|
-
* only when `tools` or `delegateTools` names it. Throws, naming the file,
|
|
717
|
-
* on an invalid setting.
|
|
829
|
+
* named tools, or the parent's current tools (including indirect callable
|
|
830
|
+
* tools when nested, less any `-name` entries), then `delegateTools`.
|
|
831
|
+
* Throws, naming the file, on an invalid setting.
|
|
718
832
|
*/
|
|
719
833
|
function childTools(agent: Agent): string[] {
|
|
720
|
-
|
|
834
|
+
const selected = toolSelection(
|
|
835
|
+
agent,
|
|
836
|
+
agent.tools === undefined ? (identity.current() ? availableTools(pi) : pi.getActiveTools()) : [],
|
|
837
|
+
delegateTools(getAgentDir()),
|
|
838
|
+
);
|
|
839
|
+
if (grantsNesting(agent, (identity.current()?.depth ?? 0) + 1)) selected.push(...DELEGATION_TOOLS);
|
|
840
|
+
return [...new Set(selected)];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function coordinates(roster: Roster): boolean {
|
|
844
|
+
const own = identity.current();
|
|
845
|
+
const agent = roster.agents.find((item) => item.name === own?.agent);
|
|
846
|
+
return own !== undefined && agent !== undefined && grantsNesting(agent, own.depth);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function admit(agent: string, roster: Roster): void {
|
|
850
|
+
owner.assert();
|
|
851
|
+
results.checkStorage();
|
|
852
|
+
identity.admit(agent, roster);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function exposeDelegation(roster: Roster): void {
|
|
856
|
+
if (!identity.current()) return;
|
|
857
|
+
const enabled = coordinates(roster);
|
|
858
|
+
const base = pi.getActiveTools().filter((name) => !DELEGATION_TOOLS.has(name));
|
|
859
|
+
pi.setActiveTools(enabled ? [...base, ...DELEGATION_TOOLS] : base);
|
|
721
860
|
}
|
|
722
861
|
|
|
723
862
|
/** Read the roster now, warning about every file left out. */
|
|
@@ -744,6 +883,8 @@ export default function delegate(
|
|
|
744
883
|
|
|
745
884
|
/** The request as a start, or an error naming what to fix. */
|
|
746
885
|
async function validate(request: Parsed, roster: Roster, ctx: ExtensionContext): Promise<Start> {
|
|
886
|
+
admit(request.agent, roster);
|
|
887
|
+
results.checkStorage(true);
|
|
747
888
|
const disabled = disabledProblem(roster, request.agent);
|
|
748
889
|
if (disabled !== undefined) throw new Error(disabled);
|
|
749
890
|
if (roster.agents.length === 0) throw new Error(noAgents());
|
|
@@ -820,6 +961,8 @@ export default function delegate(
|
|
|
820
961
|
/** The delegate's tools, from `toolSelection`, read when the launch began; empty means none. */
|
|
821
962
|
tools: readonly string[];
|
|
822
963
|
onWindowOpened?: () => void;
|
|
964
|
+
identity: Identity;
|
|
965
|
+
sessionId?: string;
|
|
823
966
|
}): Promise<OpenedWindow> {
|
|
824
967
|
const {
|
|
825
968
|
id,
|
|
@@ -836,14 +979,24 @@ export default function delegate(
|
|
|
836
979
|
tools,
|
|
837
980
|
onWindowOpened,
|
|
838
981
|
} = input;
|
|
839
|
-
|
|
982
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
983
|
+
results.checkStorage(true);
|
|
984
|
+
if (input.sessionId && sessionIsOwned(input.sessionId))
|
|
985
|
+
throw new Error(`delegate session ${input.sessionId} is already open; resume refused before spawn`);
|
|
986
|
+
const own = input.identity;
|
|
987
|
+
assertRootPlacement(own.rootPlacement);
|
|
988
|
+
own.autoExit = start.autoExit;
|
|
989
|
+
writeIdentity(input.sessionId ?? id, own);
|
|
990
|
+
const parent = own.parent;
|
|
840
991
|
const managedContext =
|
|
841
992
|
workspace.mode === "managed"
|
|
842
993
|
? `\n\nYour managed worktree is ${workspace.cwd}, branch ${workspace.createdBranch}, based on commit ${workspace.baseCommit}.`
|
|
843
994
|
: workspace.mode === "existing"
|
|
844
995
|
? `\n\nThe session that started you chose your working directory: ${workspace.cwd}`
|
|
845
996
|
: "";
|
|
846
|
-
const prompt = writePromptFile(
|
|
997
|
+
const prompt = writePromptFile(
|
|
998
|
+
delegatePrompt(start.agent, parent, managedContext, grantsNesting(start.agent, own.depth)),
|
|
999
|
+
);
|
|
847
1000
|
const argv = [
|
|
848
1001
|
...parentCommand(),
|
|
849
1002
|
...session,
|
|
@@ -855,14 +1008,18 @@ export default function delegate(
|
|
|
855
1008
|
start.model,
|
|
856
1009
|
"--thinking",
|
|
857
1010
|
start.thinking,
|
|
858
|
-
...start.agent.extensions.flatMap((extension) => ["-e", extension]),
|
|
859
1011
|
...toolArgs(tools),
|
|
860
1012
|
"--append-system-prompt",
|
|
861
1013
|
prompt,
|
|
862
1014
|
];
|
|
863
1015
|
const env: Record<string, string> = {
|
|
864
1016
|
PI_CODING_AGENT_DIR: getAgentDir(),
|
|
1017
|
+
// Private live metadata correlation: resume retains session id, not launch id.
|
|
1018
|
+
PI_SQUIRE_LIVE_LAUNCH: JSON.stringify({ version: 1, sessionId: input.sessionId ?? id, launchId: id }),
|
|
865
1019
|
[PARENT_ENV]: parent,
|
|
1020
|
+
PI_DELEGATE_ROOT: own.root,
|
|
1021
|
+
PI_DELEGATE_DEPTH: String(own.depth),
|
|
1022
|
+
PI_DELEGATE_CHAIN: own.chain.join(","),
|
|
866
1023
|
[AUTO_EXIT_ENV]: start.autoExit ? "1" : "0",
|
|
867
1024
|
[AGENT_ENV]: start.agent.name,
|
|
868
1025
|
// A general signal for any extension: no human watches this session, so don't wait on a dialog.
|
|
@@ -873,22 +1030,72 @@ export default function delegate(
|
|
|
873
1030
|
const envSessionDir = process.env[SESSION_DIR_ENV];
|
|
874
1031
|
if (envSessionDir !== undefined && envSessionDir !== "") env[SESSION_DIR_ENV] = envSessionDir;
|
|
875
1032
|
// Listeners may only append to `args` and add to `env`; there is no veto.
|
|
876
|
-
const payload: LaunchPayload = {
|
|
1033
|
+
const payload: LaunchPayload = {
|
|
1034
|
+
args: argv,
|
|
1035
|
+
env,
|
|
1036
|
+
agent: start.agent.name,
|
|
1037
|
+
cwd,
|
|
1038
|
+
toolCallId,
|
|
1039
|
+
parent: own.parent,
|
|
1040
|
+
root: own.root,
|
|
1041
|
+
depth: own.depth,
|
|
1042
|
+
chain: Object.freeze([...own.chain]),
|
|
1043
|
+
};
|
|
1044
|
+
for (const key of ["parent", "root", "depth", "chain"] as const)
|
|
1045
|
+
Object.defineProperty(payload, key, { writable: false, configurable: false });
|
|
877
1046
|
pi.events.emit(LAUNCH, payload);
|
|
1047
|
+
if (identity.current()) {
|
|
1048
|
+
const prefix = parentCommand().length;
|
|
1049
|
+
const args = payload.args.slice(prefix);
|
|
1050
|
+
enforceToolCeiling({
|
|
1051
|
+
args,
|
|
1052
|
+
available: availableTools(pi).filter(
|
|
1053
|
+
(name) => grantsNesting(start.agent, own.depth) || !DELEGATION_TOOLS.has(name),
|
|
1054
|
+
),
|
|
1055
|
+
registered: pi.getAllTools().map((tool) => tool.name),
|
|
1056
|
+
cwd,
|
|
1057
|
+
agentDir: payload.env.PI_CODING_AGENT_DIR,
|
|
1058
|
+
trusted,
|
|
1059
|
+
notices: start.notices,
|
|
1060
|
+
});
|
|
1061
|
+
payload.args.splice(prefix, payload.args.length - prefix, ...args);
|
|
1062
|
+
}
|
|
1063
|
+
const token: Reservation = { root: own.root, id };
|
|
1064
|
+
bindTree(token, { windowName: name, cwd, workspace });
|
|
1065
|
+
dispatchTree(token);
|
|
878
1066
|
let opened: OpenedWindow;
|
|
879
1067
|
try {
|
|
880
|
-
opened = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env }, placement);
|
|
881
|
-
onWindowOpened?.();
|
|
1068
|
+
opened = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env }, placement, own.rootPlacement);
|
|
882
1069
|
} catch (err) {
|
|
1070
|
+
if (err instanceof WindowOpenError && err.certainty === "not-opened") {
|
|
1071
|
+
try {
|
|
1072
|
+
failedTree(token, true);
|
|
1073
|
+
} catch (accounting) {
|
|
1074
|
+
ctx.ui.notify(
|
|
1075
|
+
`delegate: could not refund the pre-start reservation: ${String(accounting)}; it remains charged.`,
|
|
1076
|
+
"warning",
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
883
1080
|
if (err instanceof WindowOpenError) {
|
|
884
1081
|
throw new WindowOpenError(`could not open window ${name} for ${id}: ${err.message}`, err.certainty);
|
|
885
1082
|
}
|
|
886
1083
|
throw new WindowOpenError(`could not open window ${name} for ${id}: ${(err as Error).message}`, "unknown");
|
|
887
1084
|
}
|
|
1085
|
+
onWindowOpened?.();
|
|
1086
|
+
try {
|
|
1087
|
+
openedTree(token, opened.id);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
ctx.ui.notify(
|
|
1090
|
+
`delegate: window ${opened.id} opened, but shared accounting publication failed: ${String(error)}. Local tracking continues; the reservation remains charged until accounting reconciles.`,
|
|
1091
|
+
"warning",
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
888
1094
|
return opened;
|
|
889
1095
|
}
|
|
890
1096
|
|
|
891
1097
|
async function launchWindow(input: {
|
|
1098
|
+
identity: Identity;
|
|
892
1099
|
id: string;
|
|
893
1100
|
requestId: string;
|
|
894
1101
|
workspace: Workspace;
|
|
@@ -909,6 +1116,7 @@ export default function delegate(
|
|
|
909
1116
|
id,
|
|
910
1117
|
name,
|
|
911
1118
|
session: ["--session-id", id, "--name", name],
|
|
1119
|
+
identity: input.identity,
|
|
912
1120
|
workspace,
|
|
913
1121
|
trusted,
|
|
914
1122
|
cwd,
|
|
@@ -924,6 +1132,7 @@ export default function delegate(
|
|
|
924
1132
|
results.recordStart(
|
|
925
1133
|
{
|
|
926
1134
|
id,
|
|
1135
|
+
identity: readIdentity(id),
|
|
927
1136
|
agent: start.agent.name,
|
|
928
1137
|
...(start.agent.displayName === undefined ? {} : { displayName: start.agent.displayName }),
|
|
929
1138
|
model: start.model,
|
|
@@ -949,10 +1158,20 @@ export default function delegate(
|
|
|
949
1158
|
signal?: AbortSignal,
|
|
950
1159
|
toolCallId?: string,
|
|
951
1160
|
): Promise<Launched> {
|
|
1161
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1162
|
+
results.checkStorage(true);
|
|
952
1163
|
if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
|
|
953
1164
|
await requireTmuxVersion(tmux);
|
|
954
1165
|
// Read once, before anything is created or mailed; the launch keeps these values.
|
|
955
1166
|
const placement = windowPlacement(getAgentDir());
|
|
1167
|
+
const parentIdentity = identity.current();
|
|
1168
|
+
const project = parentIdentity
|
|
1169
|
+
? { companion: parentIdentity.rootPlacement, notices: [] }
|
|
1170
|
+
: placementProject(ctx.cwd);
|
|
1171
|
+
assertRootPlacement(project.companion);
|
|
1172
|
+
const own = identity.child(start.agent, ctx, project.companion);
|
|
1173
|
+
if (parentIdentity) assertParentPlacement(readTree(own.root), own);
|
|
1174
|
+
if (placement === "companion") start.notices.push(...project.notices);
|
|
956
1175
|
const tools = childTools(start.agent);
|
|
957
1176
|
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
958
1177
|
if (!isAbsolute(sessionDir))
|
|
@@ -985,13 +1204,23 @@ export default function delegate(
|
|
|
985
1204
|
getAgentDir(),
|
|
986
1205
|
);
|
|
987
1206
|
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
988
|
-
const trust = await trustPlannedWorktree(
|
|
1207
|
+
const trust = await trustPlannedWorktree(
|
|
1208
|
+
plan,
|
|
1209
|
+
ctx,
|
|
1210
|
+
getAgentDir(),
|
|
1211
|
+
combined,
|
|
1212
|
+
trustControl.forAgent(start.agent.name, ctx),
|
|
1213
|
+
);
|
|
989
1214
|
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
990
1215
|
trusted = trust.trusted;
|
|
991
1216
|
revalidateTrust = trust.revalidate;
|
|
992
1217
|
plan.trusted = trusted;
|
|
993
1218
|
} else {
|
|
994
|
-
const prepared = await prepareDirectory(
|
|
1219
|
+
const prepared = await prepareDirectory(
|
|
1220
|
+
{ cwd: cwdOption, signal, trust: trustControl.forAgent(start.agent.name, ctx) },
|
|
1221
|
+
ctx,
|
|
1222
|
+
getAgentDir(),
|
|
1223
|
+
);
|
|
995
1224
|
directory = prepared.workspace;
|
|
996
1225
|
trusted = prepared.trusted;
|
|
997
1226
|
}
|
|
@@ -1000,7 +1229,14 @@ export default function delegate(
|
|
|
1000
1229
|
let attempt: WorkspaceAttempt | undefined;
|
|
1001
1230
|
let managed: Extract<Workspace, { mode: "managed" }> | undefined;
|
|
1002
1231
|
let windowOpened = false;
|
|
1232
|
+
let reservation: Reservation | undefined;
|
|
1003
1233
|
const run = async (): Promise<Launched> => {
|
|
1234
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1235
|
+
results.checkStorage(true);
|
|
1236
|
+
await reconcileTree(own.root, tmux);
|
|
1237
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1238
|
+
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
1239
|
+
reservation = reserveTree({ id, sessionId: id, identity: own, sessionDir });
|
|
1004
1240
|
if (plan !== undefined) {
|
|
1005
1241
|
revalidate(plan);
|
|
1006
1242
|
revalidateTrust?.();
|
|
@@ -1050,11 +1286,13 @@ export default function delegate(
|
|
|
1050
1286
|
// at session start without a handshake, and a window that fails leaves
|
|
1051
1287
|
// mail that is reported rather than cleaned up.
|
|
1052
1288
|
const requestId = sendTask(id, start.task);
|
|
1289
|
+
bindTree(reservation, { requestId });
|
|
1053
1290
|
if (attempt !== undefined) {
|
|
1054
1291
|
attempt = { ...attempt, requestId };
|
|
1055
1292
|
results.recordAttempt(attempt, ctx);
|
|
1056
1293
|
}
|
|
1057
1294
|
return launchWindow({
|
|
1295
|
+
identity: own,
|
|
1058
1296
|
id,
|
|
1059
1297
|
requestId,
|
|
1060
1298
|
workspace,
|
|
@@ -1075,6 +1313,13 @@ export default function delegate(
|
|
|
1075
1313
|
try {
|
|
1076
1314
|
return await run();
|
|
1077
1315
|
} catch (error) {
|
|
1316
|
+
if (reservation) {
|
|
1317
|
+
try {
|
|
1318
|
+
failedTree(reservation);
|
|
1319
|
+
} catch (accounting) {
|
|
1320
|
+
ctx.ui.notify(`delegate: reservation remains charged: ${String(accounting)}`, "error");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1078
1323
|
if (attempt !== undefined && managed !== undefined) {
|
|
1079
1324
|
const cause = (error as Error).message;
|
|
1080
1325
|
if (windowOpened || (error instanceof WindowOpenError && error.certainty === "unknown")) {
|
|
@@ -1139,17 +1384,55 @@ export default function delegate(
|
|
|
1139
1384
|
startPolling: tracking.startPolling,
|
|
1140
1385
|
placement: () => windowPlacement(getAgentDir()),
|
|
1141
1386
|
roster: rosterFor,
|
|
1387
|
+
admit: (agent, ctx) => admit(agent, currentRoster(ctx)),
|
|
1388
|
+
authorize: (id, ctx) => {
|
|
1389
|
+
owner.assert();
|
|
1390
|
+
results.checkStorage();
|
|
1391
|
+
const record = results.find(id) ?? viewResults.find(id) ?? historicalRecord(id, ctx);
|
|
1392
|
+
if (record && isStarted(record)) assertResumeAuthority(record, ctx);
|
|
1393
|
+
},
|
|
1394
|
+
reserve: async (record, id, ctx) => {
|
|
1395
|
+
const own = record.identity;
|
|
1396
|
+
if (!own) throw new Error(`delegation ${record.id} has no original root placement identity; resume refused`);
|
|
1397
|
+
assertRootPlacement(own.rootPlacement);
|
|
1398
|
+
await reconcileTree(own.root, tmux);
|
|
1399
|
+
admit(record.agent, currentRoster(ctx));
|
|
1400
|
+
return reserveTree({
|
|
1401
|
+
id,
|
|
1402
|
+
sessionId: sessionIdOf(record),
|
|
1403
|
+
identity: own,
|
|
1404
|
+
resumedFrom: record.id,
|
|
1405
|
+
sessionDir: record.sessionDir,
|
|
1406
|
+
});
|
|
1407
|
+
},
|
|
1142
1408
|
parentDefaults,
|
|
1143
1409
|
chooseModel: (explicit, agent, models, preferred) =>
|
|
1144
1410
|
chooseModel(explicit, agent, models, proactive, agentModels(agent, undefined), preferred),
|
|
1145
1411
|
thinkingProblem,
|
|
1146
|
-
prepareDirectory: (cwd, ctx, signal) =>
|
|
1412
|
+
prepareDirectory: (cwd, ctx, signal, agent) =>
|
|
1413
|
+
prepareDirectory({ cwd, signal, trust: trustControl.forAgent(agent, ctx) }, ctx, getAgentDir()),
|
|
1147
1414
|
sendTask,
|
|
1148
|
-
open: ({
|
|
1415
|
+
open: ({
|
|
1416
|
+
id,
|
|
1417
|
+
name,
|
|
1418
|
+
sessionPath,
|
|
1419
|
+
workspace,
|
|
1420
|
+
trusted,
|
|
1421
|
+
cwd,
|
|
1422
|
+
sessionDir,
|
|
1423
|
+
start,
|
|
1424
|
+
toolCallId,
|
|
1425
|
+
ctx,
|
|
1426
|
+
placement,
|
|
1427
|
+
identity: own,
|
|
1428
|
+
sessionId,
|
|
1429
|
+
}) =>
|
|
1149
1430
|
openChild({
|
|
1150
1431
|
id,
|
|
1151
1432
|
name,
|
|
1152
1433
|
session: ["--session", sessionPath],
|
|
1434
|
+
identity: own,
|
|
1435
|
+
sessionId,
|
|
1153
1436
|
workspace,
|
|
1154
1437
|
trusted,
|
|
1155
1438
|
cwd,
|
|
@@ -1219,9 +1502,10 @@ export default function delegate(
|
|
|
1219
1502
|
await runDelegate(outcome.parsed, ctx);
|
|
1220
1503
|
}
|
|
1221
1504
|
|
|
1222
|
-
async function
|
|
1223
|
-
|
|
1224
|
-
|
|
1505
|
+
async function resumePicker(ctx: ExtensionContext, id?: string): Promise<void> {
|
|
1506
|
+
await pickResume(
|
|
1507
|
+
ctx,
|
|
1508
|
+
{
|
|
1225
1509
|
unavailable: () => (tmux.insideTmux() ? undefined : NOT_IN_TMUX),
|
|
1226
1510
|
eligible: resumer.eligible,
|
|
1227
1511
|
resume: async (request, context) => {
|
|
@@ -1231,9 +1515,13 @@ export default function delegate(
|
|
|
1231
1515
|
context.ui.notify(`delegate: ${(err as Error).message}`, "error");
|
|
1232
1516
|
}
|
|
1233
1517
|
},
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1518
|
+
},
|
|
1519
|
+
id,
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
async function resumeCommand(input: string, ctx: ExtensionContext): Promise<void> {
|
|
1524
|
+
if (input.trim() === "") return resumePicker(ctx);
|
|
1237
1525
|
const outcome = parseResume(input);
|
|
1238
1526
|
if ("error" in outcome) {
|
|
1239
1527
|
ctx.ui.notify(`delegate: ${outcome.error}`, "error");
|
|
@@ -1382,23 +1670,28 @@ export default function delegate(
|
|
|
1382
1670
|
pi.on("session_start", (_event, ctx) => {
|
|
1383
1671
|
registry = ctx.modelRegistry;
|
|
1384
1672
|
latest = ctx;
|
|
1385
|
-
tracking.restore(ctx);
|
|
1386
1673
|
// The roster is read at call time; the description is rebuilt here and before each run.
|
|
1387
1674
|
const roster = currentRoster(ctx);
|
|
1388
1675
|
announced.clear();
|
|
1389
1676
|
announce(roster, ctx);
|
|
1390
1677
|
registerDelegate(toolDescription(roster.agents));
|
|
1678
|
+
exposeDelegation(roster);
|
|
1391
1679
|
});
|
|
1392
1680
|
|
|
1393
1681
|
// One roster/prompt snapshot prepares this run: refresh the description only when
|
|
1394
1682
|
// it changed, and replace only this extension's prompt section from the same snapshot.
|
|
1395
1683
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1396
1684
|
latest = ctx;
|
|
1685
|
+
const own = identity.current();
|
|
1397
1686
|
const roster = currentRoster(ctx);
|
|
1687
|
+
if (own)
|
|
1688
|
+
event.systemPromptOptions.sections.squire_delegate_task = `You are a delegate. ${parentTrust(own.parent, coordinates(roster))}`;
|
|
1689
|
+
else delete event.systemPromptOptions.sections.squire_delegate_task;
|
|
1690
|
+
exposeDelegation(roster);
|
|
1398
1691
|
announce(roster, ctx);
|
|
1399
1692
|
const description = toolDescription(roster.agents);
|
|
1400
1693
|
const prompt = readParentPrompt();
|
|
1401
|
-
const visible = delegateVisible(event.systemPromptOptions);
|
|
1694
|
+
const visible = delegateVisible(event.systemPromptOptions) && pi.getActiveTools().includes("delegate");
|
|
1402
1695
|
// Report a read failure even when guidance cannot be used, so it does not go unseen.
|
|
1403
1696
|
if (prompt.error !== undefined) ctx.ui.notify(`delegate: ${prompt.error}`, "warning");
|
|
1404
1697
|
if (visible && description !== registeredDescription) registerDelegate(description);
|
|
@@ -1412,6 +1705,7 @@ export default function delegate(
|
|
|
1412
1705
|
pi.on("session_shutdown", () => {
|
|
1413
1706
|
tracking.stop();
|
|
1414
1707
|
});
|
|
1708
|
+
host.on("session_shutdown", () => owner.stop());
|
|
1415
1709
|
|
|
1416
1710
|
/** Agent names, flags, model ids and thinking levels, at the cursor's token. */
|
|
1417
1711
|
function completions(prefix: string): { value: string; label: string }[] | null {
|
|
@@ -1455,12 +1749,20 @@ export default function delegate(
|
|
|
1455
1749
|
handler: resumeCommand,
|
|
1456
1750
|
});
|
|
1457
1751
|
|
|
1458
|
-
const navigator = createNavigator(
|
|
1459
|
-
|
|
1752
|
+
const navigator = createNavigator(
|
|
1753
|
+
viewResults,
|
|
1754
|
+
tmux,
|
|
1755
|
+
tracking,
|
|
1756
|
+
(ctx: ExtensionContext) => startNewDelegate(ctx, newDelegateDeps),
|
|
1757
|
+
{
|
|
1758
|
+
assertActive: () => owner.assert(),
|
|
1759
|
+
close: (id, ctx) => tracking.close(id, ctx),
|
|
1760
|
+
resume: (id, ctx) => resumePicker(ctx, id),
|
|
1761
|
+
},
|
|
1460
1762
|
);
|
|
1461
1763
|
|
|
1462
1764
|
pi.registerCommand("delegates", {
|
|
1463
|
-
description: "Open the
|
|
1765
|
+
description: "Open the shared delegate dashboard: details, switch, peek, close, resume",
|
|
1464
1766
|
handler: async (_args: string, ctx: ExtensionContext) => navigator.panel(ctx),
|
|
1465
1767
|
});
|
|
1466
1768
|
|