pi-squire 0.15.0 → 0.17.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 +343 -1
- package/README.md +931 -118
- package/package.json +2 -1
- package/src/agents.ts +73 -43
- 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 +342 -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-bundles.ts +84 -0
- package/src/tool-names.ts +29 -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 } from "./tool-names.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,56 @@ 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
|
+
const token: Reservation = { root: own.root, id };
|
|
1048
|
+
bindTree(token, { windowName: name, cwd, workspace });
|
|
1049
|
+
dispatchTree(token);
|
|
878
1050
|
let opened: OpenedWindow;
|
|
879
1051
|
try {
|
|
880
|
-
opened = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env }, placement);
|
|
881
|
-
onWindowOpened?.();
|
|
1052
|
+
opened = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env }, placement, own.rootPlacement);
|
|
882
1053
|
} catch (err) {
|
|
1054
|
+
if (err instanceof WindowOpenError && err.certainty === "not-opened") {
|
|
1055
|
+
try {
|
|
1056
|
+
failedTree(token, true);
|
|
1057
|
+
} catch (accounting) {
|
|
1058
|
+
ctx.ui.notify(
|
|
1059
|
+
`delegate: could not refund the pre-start reservation: ${String(accounting)}; it remains charged.`,
|
|
1060
|
+
"warning",
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
883
1064
|
if (err instanceof WindowOpenError) {
|
|
884
1065
|
throw new WindowOpenError(`could not open window ${name} for ${id}: ${err.message}`, err.certainty);
|
|
885
1066
|
}
|
|
886
1067
|
throw new WindowOpenError(`could not open window ${name} for ${id}: ${(err as Error).message}`, "unknown");
|
|
887
1068
|
}
|
|
1069
|
+
onWindowOpened?.();
|
|
1070
|
+
try {
|
|
1071
|
+
openedTree(token, opened.id);
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
ctx.ui.notify(
|
|
1074
|
+
`delegate: window ${opened.id} opened, but shared accounting publication failed: ${String(error)}. Local tracking continues; the reservation remains charged until accounting reconciles.`,
|
|
1075
|
+
"warning",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
888
1078
|
return opened;
|
|
889
1079
|
}
|
|
890
1080
|
|
|
891
1081
|
async function launchWindow(input: {
|
|
1082
|
+
identity: Identity;
|
|
892
1083
|
id: string;
|
|
893
1084
|
requestId: string;
|
|
894
1085
|
workspace: Workspace;
|
|
@@ -909,6 +1100,7 @@ export default function delegate(
|
|
|
909
1100
|
id,
|
|
910
1101
|
name,
|
|
911
1102
|
session: ["--session-id", id, "--name", name],
|
|
1103
|
+
identity: input.identity,
|
|
912
1104
|
workspace,
|
|
913
1105
|
trusted,
|
|
914
1106
|
cwd,
|
|
@@ -924,6 +1116,7 @@ export default function delegate(
|
|
|
924
1116
|
results.recordStart(
|
|
925
1117
|
{
|
|
926
1118
|
id,
|
|
1119
|
+
identity: readIdentity(id),
|
|
927
1120
|
agent: start.agent.name,
|
|
928
1121
|
...(start.agent.displayName === undefined ? {} : { displayName: start.agent.displayName }),
|
|
929
1122
|
model: start.model,
|
|
@@ -949,10 +1142,20 @@ export default function delegate(
|
|
|
949
1142
|
signal?: AbortSignal,
|
|
950
1143
|
toolCallId?: string,
|
|
951
1144
|
): Promise<Launched> {
|
|
1145
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1146
|
+
results.checkStorage(true);
|
|
952
1147
|
if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
|
|
953
1148
|
await requireTmuxVersion(tmux);
|
|
954
1149
|
// Read once, before anything is created or mailed; the launch keeps these values.
|
|
955
1150
|
const placement = windowPlacement(getAgentDir());
|
|
1151
|
+
const parentIdentity = identity.current();
|
|
1152
|
+
const project = parentIdentity
|
|
1153
|
+
? { companion: parentIdentity.rootPlacement, notices: [] }
|
|
1154
|
+
: placementProject(ctx.cwd);
|
|
1155
|
+
assertRootPlacement(project.companion);
|
|
1156
|
+
const own = identity.child(start.agent, ctx, project.companion);
|
|
1157
|
+
if (parentIdentity) assertParentPlacement(readTree(own.root), own);
|
|
1158
|
+
if (placement === "companion") start.notices.push(...project.notices);
|
|
956
1159
|
const tools = childTools(start.agent);
|
|
957
1160
|
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
958
1161
|
if (!isAbsolute(sessionDir))
|
|
@@ -985,13 +1188,23 @@ export default function delegate(
|
|
|
985
1188
|
getAgentDir(),
|
|
986
1189
|
);
|
|
987
1190
|
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
988
|
-
const trust = await trustPlannedWorktree(
|
|
1191
|
+
const trust = await trustPlannedWorktree(
|
|
1192
|
+
plan,
|
|
1193
|
+
ctx,
|
|
1194
|
+
getAgentDir(),
|
|
1195
|
+
combined,
|
|
1196
|
+
trustControl.forAgent(start.agent.name, ctx),
|
|
1197
|
+
);
|
|
989
1198
|
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
990
1199
|
trusted = trust.trusted;
|
|
991
1200
|
revalidateTrust = trust.revalidate;
|
|
992
1201
|
plan.trusted = trusted;
|
|
993
1202
|
} else {
|
|
994
|
-
const prepared = await prepareDirectory(
|
|
1203
|
+
const prepared = await prepareDirectory(
|
|
1204
|
+
{ cwd: cwdOption, signal, trust: trustControl.forAgent(start.agent.name, ctx) },
|
|
1205
|
+
ctx,
|
|
1206
|
+
getAgentDir(),
|
|
1207
|
+
);
|
|
995
1208
|
directory = prepared.workspace;
|
|
996
1209
|
trusted = prepared.trusted;
|
|
997
1210
|
}
|
|
@@ -1000,7 +1213,14 @@ export default function delegate(
|
|
|
1000
1213
|
let attempt: WorkspaceAttempt | undefined;
|
|
1001
1214
|
let managed: Extract<Workspace, { mode: "managed" }> | undefined;
|
|
1002
1215
|
let windowOpened = false;
|
|
1216
|
+
let reservation: Reservation | undefined;
|
|
1003
1217
|
const run = async (): Promise<Launched> => {
|
|
1218
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1219
|
+
results.checkStorage(true);
|
|
1220
|
+
await reconcileTree(own.root, tmux);
|
|
1221
|
+
admit(start.agent.name, currentRoster(ctx));
|
|
1222
|
+
if (combined?.aborted) throw new Error("the launch was aborted");
|
|
1223
|
+
reservation = reserveTree({ id, sessionId: id, identity: own, sessionDir });
|
|
1004
1224
|
if (plan !== undefined) {
|
|
1005
1225
|
revalidate(plan);
|
|
1006
1226
|
revalidateTrust?.();
|
|
@@ -1050,11 +1270,13 @@ export default function delegate(
|
|
|
1050
1270
|
// at session start without a handshake, and a window that fails leaves
|
|
1051
1271
|
// mail that is reported rather than cleaned up.
|
|
1052
1272
|
const requestId = sendTask(id, start.task);
|
|
1273
|
+
bindTree(reservation, { requestId });
|
|
1053
1274
|
if (attempt !== undefined) {
|
|
1054
1275
|
attempt = { ...attempt, requestId };
|
|
1055
1276
|
results.recordAttempt(attempt, ctx);
|
|
1056
1277
|
}
|
|
1057
1278
|
return launchWindow({
|
|
1279
|
+
identity: own,
|
|
1058
1280
|
id,
|
|
1059
1281
|
requestId,
|
|
1060
1282
|
workspace,
|
|
@@ -1075,6 +1297,13 @@ export default function delegate(
|
|
|
1075
1297
|
try {
|
|
1076
1298
|
return await run();
|
|
1077
1299
|
} catch (error) {
|
|
1300
|
+
if (reservation) {
|
|
1301
|
+
try {
|
|
1302
|
+
failedTree(reservation);
|
|
1303
|
+
} catch (accounting) {
|
|
1304
|
+
ctx.ui.notify(`delegate: reservation remains charged: ${String(accounting)}`, "error");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1078
1307
|
if (attempt !== undefined && managed !== undefined) {
|
|
1079
1308
|
const cause = (error as Error).message;
|
|
1080
1309
|
if (windowOpened || (error instanceof WindowOpenError && error.certainty === "unknown")) {
|
|
@@ -1139,17 +1368,55 @@ export default function delegate(
|
|
|
1139
1368
|
startPolling: tracking.startPolling,
|
|
1140
1369
|
placement: () => windowPlacement(getAgentDir()),
|
|
1141
1370
|
roster: rosterFor,
|
|
1371
|
+
admit: (agent, ctx) => admit(agent, currentRoster(ctx)),
|
|
1372
|
+
authorize: (id, ctx) => {
|
|
1373
|
+
owner.assert();
|
|
1374
|
+
results.checkStorage();
|
|
1375
|
+
const record = results.find(id) ?? viewResults.find(id) ?? historicalRecord(id, ctx);
|
|
1376
|
+
if (record && isStarted(record)) assertResumeAuthority(record, ctx);
|
|
1377
|
+
},
|
|
1378
|
+
reserve: async (record, id, ctx) => {
|
|
1379
|
+
const own = record.identity;
|
|
1380
|
+
if (!own) throw new Error(`delegation ${record.id} has no original root placement identity; resume refused`);
|
|
1381
|
+
assertRootPlacement(own.rootPlacement);
|
|
1382
|
+
await reconcileTree(own.root, tmux);
|
|
1383
|
+
admit(record.agent, currentRoster(ctx));
|
|
1384
|
+
return reserveTree({
|
|
1385
|
+
id,
|
|
1386
|
+
sessionId: sessionIdOf(record),
|
|
1387
|
+
identity: own,
|
|
1388
|
+
resumedFrom: record.id,
|
|
1389
|
+
sessionDir: record.sessionDir,
|
|
1390
|
+
});
|
|
1391
|
+
},
|
|
1142
1392
|
parentDefaults,
|
|
1143
1393
|
chooseModel: (explicit, agent, models, preferred) =>
|
|
1144
1394
|
chooseModel(explicit, agent, models, proactive, agentModels(agent, undefined), preferred),
|
|
1145
1395
|
thinkingProblem,
|
|
1146
|
-
prepareDirectory: (cwd, ctx, signal) =>
|
|
1396
|
+
prepareDirectory: (cwd, ctx, signal, agent) =>
|
|
1397
|
+
prepareDirectory({ cwd, signal, trust: trustControl.forAgent(agent, ctx) }, ctx, getAgentDir()),
|
|
1147
1398
|
sendTask,
|
|
1148
|
-
open: ({
|
|
1399
|
+
open: ({
|
|
1400
|
+
id,
|
|
1401
|
+
name,
|
|
1402
|
+
sessionPath,
|
|
1403
|
+
workspace,
|
|
1404
|
+
trusted,
|
|
1405
|
+
cwd,
|
|
1406
|
+
sessionDir,
|
|
1407
|
+
start,
|
|
1408
|
+
toolCallId,
|
|
1409
|
+
ctx,
|
|
1410
|
+
placement,
|
|
1411
|
+
identity: own,
|
|
1412
|
+
sessionId,
|
|
1413
|
+
}) =>
|
|
1149
1414
|
openChild({
|
|
1150
1415
|
id,
|
|
1151
1416
|
name,
|
|
1152
1417
|
session: ["--session", sessionPath],
|
|
1418
|
+
identity: own,
|
|
1419
|
+
sessionId,
|
|
1153
1420
|
workspace,
|
|
1154
1421
|
trusted,
|
|
1155
1422
|
cwd,
|
|
@@ -1219,9 +1486,10 @@ export default function delegate(
|
|
|
1219
1486
|
await runDelegate(outcome.parsed, ctx);
|
|
1220
1487
|
}
|
|
1221
1488
|
|
|
1222
|
-
async function
|
|
1223
|
-
|
|
1224
|
-
|
|
1489
|
+
async function resumePicker(ctx: ExtensionContext, id?: string): Promise<void> {
|
|
1490
|
+
await pickResume(
|
|
1491
|
+
ctx,
|
|
1492
|
+
{
|
|
1225
1493
|
unavailable: () => (tmux.insideTmux() ? undefined : NOT_IN_TMUX),
|
|
1226
1494
|
eligible: resumer.eligible,
|
|
1227
1495
|
resume: async (request, context) => {
|
|
@@ -1231,9 +1499,13 @@ export default function delegate(
|
|
|
1231
1499
|
context.ui.notify(`delegate: ${(err as Error).message}`, "error");
|
|
1232
1500
|
}
|
|
1233
1501
|
},
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1502
|
+
},
|
|
1503
|
+
id,
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
async function resumeCommand(input: string, ctx: ExtensionContext): Promise<void> {
|
|
1508
|
+
if (input.trim() === "") return resumePicker(ctx);
|
|
1237
1509
|
const outcome = parseResume(input);
|
|
1238
1510
|
if ("error" in outcome) {
|
|
1239
1511
|
ctx.ui.notify(`delegate: ${outcome.error}`, "error");
|
|
@@ -1382,23 +1654,28 @@ export default function delegate(
|
|
|
1382
1654
|
pi.on("session_start", (_event, ctx) => {
|
|
1383
1655
|
registry = ctx.modelRegistry;
|
|
1384
1656
|
latest = ctx;
|
|
1385
|
-
tracking.restore(ctx);
|
|
1386
1657
|
// The roster is read at call time; the description is rebuilt here and before each run.
|
|
1387
1658
|
const roster = currentRoster(ctx);
|
|
1388
1659
|
announced.clear();
|
|
1389
1660
|
announce(roster, ctx);
|
|
1390
1661
|
registerDelegate(toolDescription(roster.agents));
|
|
1662
|
+
exposeDelegation(roster);
|
|
1391
1663
|
});
|
|
1392
1664
|
|
|
1393
1665
|
// One roster/prompt snapshot prepares this run: refresh the description only when
|
|
1394
1666
|
// it changed, and replace only this extension's prompt section from the same snapshot.
|
|
1395
1667
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1396
1668
|
latest = ctx;
|
|
1669
|
+
const own = identity.current();
|
|
1397
1670
|
const roster = currentRoster(ctx);
|
|
1671
|
+
if (own)
|
|
1672
|
+
event.systemPromptOptions.sections.squire_delegate_task = `You are a delegate. ${parentTrust(own.parent, coordinates(roster))}`;
|
|
1673
|
+
else delete event.systemPromptOptions.sections.squire_delegate_task;
|
|
1674
|
+
exposeDelegation(roster);
|
|
1398
1675
|
announce(roster, ctx);
|
|
1399
1676
|
const description = toolDescription(roster.agents);
|
|
1400
1677
|
const prompt = readParentPrompt();
|
|
1401
|
-
const visible = delegateVisible(event.systemPromptOptions);
|
|
1678
|
+
const visible = delegateVisible(event.systemPromptOptions) && pi.getActiveTools().includes("delegate");
|
|
1402
1679
|
// Report a read failure even when guidance cannot be used, so it does not go unseen.
|
|
1403
1680
|
if (prompt.error !== undefined) ctx.ui.notify(`delegate: ${prompt.error}`, "warning");
|
|
1404
1681
|
if (visible && description !== registeredDescription) registerDelegate(description);
|
|
@@ -1412,6 +1689,7 @@ export default function delegate(
|
|
|
1412
1689
|
pi.on("session_shutdown", () => {
|
|
1413
1690
|
tracking.stop();
|
|
1414
1691
|
});
|
|
1692
|
+
host.on("session_shutdown", () => owner.stop());
|
|
1415
1693
|
|
|
1416
1694
|
/** Agent names, flags, model ids and thinking levels, at the cursor's token. */
|
|
1417
1695
|
function completions(prefix: string): { value: string; label: string }[] | null {
|
|
@@ -1455,12 +1733,20 @@ export default function delegate(
|
|
|
1455
1733
|
handler: resumeCommand,
|
|
1456
1734
|
});
|
|
1457
1735
|
|
|
1458
|
-
const navigator = createNavigator(
|
|
1459
|
-
|
|
1736
|
+
const navigator = createNavigator(
|
|
1737
|
+
viewResults,
|
|
1738
|
+
tmux,
|
|
1739
|
+
tracking,
|
|
1740
|
+
(ctx: ExtensionContext) => startNewDelegate(ctx, newDelegateDeps),
|
|
1741
|
+
{
|
|
1742
|
+
assertActive: () => owner.assert(),
|
|
1743
|
+
close: (id, ctx) => tracking.close(id, ctx),
|
|
1744
|
+
resume: (id, ctx) => resumePicker(ctx, id),
|
|
1745
|
+
},
|
|
1460
1746
|
);
|
|
1461
1747
|
|
|
1462
1748
|
pi.registerCommand("delegates", {
|
|
1463
|
-
description: "Open the
|
|
1749
|
+
description: "Open the shared delegate dashboard: details, switch, peek, close, resume",
|
|
1464
1750
|
handler: async (_args: string, ctx: ExtensionContext) => navigator.panel(ctx),
|
|
1465
1751
|
});
|
|
1466
1752
|
|