ultimate-pi 0.9.1 → 0.10.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/.agents/skills/harness-decisions/SKILL.md +17 -13
- package/.agents/skills/harness-plan/SKILL.md +3 -3
- package/.pi/agents/harness/planner.md +8 -4
- package/.pi/extensions/harness-live-widget.ts +48 -28
- package/.pi/extensions/harness-plan-approval.ts +174 -0
- package/.pi/extensions/harness-run-context.ts +38 -8
- package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +11 -1
- package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +8 -87
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +310 -0
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +59 -0
- package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +9 -0
- package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +4 -0
- package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +39 -12
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +38 -12
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +2 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +131 -0
- package/.pi/extensions/lib/plan-approval/dialog.ts +291 -0
- package/.pi/extensions/lib/plan-approval/fallback.ts +50 -0
- package/.pi/extensions/lib/plan-approval/format-plan.ts +94 -0
- package/.pi/extensions/lib/plan-approval/render.ts +83 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +39 -0
- package/.pi/extensions/lib/plan-approval/types.ts +32 -0
- package/.pi/extensions/lib/plan-approval/validate.ts +61 -0
- package/.pi/lib/harness-run-context.ts +117 -28
- package/.pi/prompts/harness-plan.md +20 -7
- package/CHANGELOG.md +12 -0
- package/package.json +3 -3
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers ask_user and approve_plan in subagent sessions, delegating UI to the parent harness session.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import type {
|
|
12
|
+
PlanPacketLike,
|
|
13
|
+
PlanUserApproval,
|
|
14
|
+
} from "../../../lib/harness-run-context.js";
|
|
15
|
+
import { parsePlanApprovalFromMessage } from "../../../lib/harness-run-context.js";
|
|
16
|
+
import { runAskDialog } from "../ask-user/dialog.js";
|
|
17
|
+
import { runAskFallback } from "../ask-user/fallback.js";
|
|
18
|
+
import { renderAskCall, renderAskResult } from "../ask-user/render.js";
|
|
19
|
+
import {
|
|
20
|
+
PROMPT_GUIDELINES as ASK_PROMPT_GUIDELINES,
|
|
21
|
+
PROMPT_SNIPPET as ASK_PROMPT_SNIPPET,
|
|
22
|
+
AskUserParamsSchema,
|
|
23
|
+
} from "../ask-user/schema.js";
|
|
24
|
+
import type { AskUserParams, DialogResult } from "../ask-user/types.js";
|
|
25
|
+
import {
|
|
26
|
+
formatResultText,
|
|
27
|
+
toToolDetails,
|
|
28
|
+
validateAskParams,
|
|
29
|
+
} from "../ask-user/validate.js";
|
|
30
|
+
import {
|
|
31
|
+
CREATE_PLAN_GUIDELINES,
|
|
32
|
+
CREATE_PLAN_SNIPPET,
|
|
33
|
+
executeCreatePlan,
|
|
34
|
+
formatCreatePlanResultText,
|
|
35
|
+
} from "../plan-approval/create-plan.js";
|
|
36
|
+
import { runPlanApprovalDialog } from "../plan-approval/dialog.js";
|
|
37
|
+
import { runPlanApprovalFallback } from "../plan-approval/fallback.js";
|
|
38
|
+
import {
|
|
39
|
+
renderApprovePlanCall,
|
|
40
|
+
renderApprovePlanResult,
|
|
41
|
+
} from "../plan-approval/render.js";
|
|
42
|
+
import {
|
|
43
|
+
ApprovePlanParamsSchema,
|
|
44
|
+
PROMPT_GUIDELINES as PLAN_PROMPT_GUIDELINES,
|
|
45
|
+
PROMPT_SNIPPET as PLAN_PROMPT_SNIPPET,
|
|
46
|
+
} from "../plan-approval/schema.js";
|
|
47
|
+
import type { ApprovePlanParams } from "../plan-approval/types.js";
|
|
48
|
+
import {
|
|
49
|
+
formatApprovePlanResultText,
|
|
50
|
+
toApprovePlanToolDetails,
|
|
51
|
+
validateApprovePlanParams,
|
|
52
|
+
} from "../plan-approval/validate.js";
|
|
53
|
+
|
|
54
|
+
const HARNESS_UI_AGENT_TYPES = new Set([
|
|
55
|
+
"harness/planner",
|
|
56
|
+
"harness/evaluator",
|
|
57
|
+
"harness/adversary",
|
|
58
|
+
"harness/tie-breaker",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export interface ParentHarnessUiHooks {
|
|
62
|
+
projectRoot?: string;
|
|
63
|
+
getParentEntries?: () => unknown[];
|
|
64
|
+
getParentRunContext?: () =>
|
|
65
|
+
| import("../../../lib/harness-run-context.js").HarnessRunContext
|
|
66
|
+
| null;
|
|
67
|
+
onPlanApproval?: (approval: PlanUserApproval) => void;
|
|
68
|
+
appendPlanDraft?: (draft: {
|
|
69
|
+
plan_packet: PlanPacketLike;
|
|
70
|
+
human_summary?: string;
|
|
71
|
+
}) => void;
|
|
72
|
+
onPlanCommitted?: (
|
|
73
|
+
runCtx: import("../../../lib/harness-run-context.js").HarnessRunContext,
|
|
74
|
+
packet: PlanPacketLike,
|
|
75
|
+
planPath: string,
|
|
76
|
+
) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const CreatePlanParamsSchema = Type.Object({
|
|
80
|
+
plan_packet: Type.Object(
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
description:
|
|
84
|
+
"Approved PlanPacket to persist (same object as approve_plan).",
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const PLANNER_ONLY_AGENT = "harness/planner";
|
|
90
|
+
|
|
91
|
+
export function agentTypeAllowsParentHarnessUi(agentType: string): boolean {
|
|
92
|
+
return HARNESS_UI_AGENT_TYPES.has(agentType);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @deprecated Use agentTypeAllowsParentHarnessUi */
|
|
96
|
+
export function agentTypeAllowsParentAskUser(agentType: string): boolean {
|
|
97
|
+
return agentTypeAllowsParentHarnessUi(agentType);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function notifyPlanApproval(
|
|
101
|
+
hooks: ParentHarnessUiHooks | undefined,
|
|
102
|
+
details: unknown,
|
|
103
|
+
toolName: "ask_user" | "approve_plan",
|
|
104
|
+
): void {
|
|
105
|
+
if (!hooks?.onPlanApproval) return;
|
|
106
|
+
const approval = parsePlanApprovalFromMessage({ toolName, details });
|
|
107
|
+
if (approval) hooks.onPlanApproval(approval);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createParentHarnessUiBridgeFactory(
|
|
111
|
+
parentCtx: ExtensionContext,
|
|
112
|
+
agentType: string,
|
|
113
|
+
hooks?: ParentHarnessUiHooks,
|
|
114
|
+
): ((pi: ExtensionAPI) => void) | null {
|
|
115
|
+
if (!agentTypeAllowsParentHarnessUi(agentType)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return (pi: ExtensionAPI) => {
|
|
119
|
+
pi.registerTool({
|
|
120
|
+
name: "ask_user",
|
|
121
|
+
label: "Ask User",
|
|
122
|
+
description:
|
|
123
|
+
"Ask the user a structured question (parent session UI). Use for clarification — not final plan approval (use approve_plan).",
|
|
124
|
+
promptSnippet: ASK_PROMPT_SNIPPET,
|
|
125
|
+
promptGuidelines: ASK_PROMPT_GUIDELINES,
|
|
126
|
+
parameters: AskUserParamsSchema,
|
|
127
|
+
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
128
|
+
const validated = validateAskParams(params as AskUserParams);
|
|
129
|
+
if (typeof validated === "string") {
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: validated }],
|
|
132
|
+
details: {
|
|
133
|
+
question: params.question ?? "",
|
|
134
|
+
options: [],
|
|
135
|
+
response: null,
|
|
136
|
+
cancelled: true,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
let outcome: DialogResult;
|
|
141
|
+
if (parentCtx.hasUI) {
|
|
142
|
+
outcome = await runAskDialog(parentCtx.ui, validated);
|
|
143
|
+
} else {
|
|
144
|
+
outcome = await runAskFallback(parentCtx.ui, validated);
|
|
145
|
+
}
|
|
146
|
+
const details = toToolDetails(
|
|
147
|
+
validated,
|
|
148
|
+
outcome.response,
|
|
149
|
+
outcome.cancelled,
|
|
150
|
+
);
|
|
151
|
+
notifyPlanApproval(hooks, details, "ask_user");
|
|
152
|
+
const text = formatResultText(outcome.response, outcome.cancelled);
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text }],
|
|
155
|
+
details,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
renderCall(args, theme) {
|
|
159
|
+
return renderAskCall(args, theme);
|
|
160
|
+
},
|
|
161
|
+
renderResult(result, options, theme) {
|
|
162
|
+
return renderAskResult(result, options, theme);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (agentType !== PLANNER_ONLY_AGENT) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pi.registerTool({
|
|
171
|
+
name: "approve_plan",
|
|
172
|
+
label: "Approve Plan",
|
|
173
|
+
description:
|
|
174
|
+
"Present the full PlanPacket for user approval in the parent TUI (scrollable overlay).",
|
|
175
|
+
promptSnippet: PLAN_PROMPT_SNIPPET,
|
|
176
|
+
promptGuidelines: PLAN_PROMPT_GUIDELINES,
|
|
177
|
+
parameters: ApprovePlanParamsSchema,
|
|
178
|
+
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
179
|
+
const validated = validateApprovePlanParams(
|
|
180
|
+
params as ApprovePlanParams,
|
|
181
|
+
);
|
|
182
|
+
if (typeof validated === "string") {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: validated }],
|
|
185
|
+
details: {
|
|
186
|
+
plan_packet: (params as ApprovePlanParams).plan_packet ?? {},
|
|
187
|
+
options: [],
|
|
188
|
+
response: null,
|
|
189
|
+
cancelled: true,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
hooks?.appendPlanDraft?.({
|
|
195
|
+
plan_packet: validated.plan_packet,
|
|
196
|
+
human_summary: validated.human_summary,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let outcome: DialogResult;
|
|
200
|
+
if (parentCtx.hasUI) {
|
|
201
|
+
outcome = await runPlanApprovalDialog(parentCtx.ui, validated, {
|
|
202
|
+
onMounted: () => {
|
|
203
|
+
pi.events.emit("plan-approval:mounted", {});
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
outcome = await runPlanApprovalFallback(parentCtx.ui, validated);
|
|
208
|
+
}
|
|
209
|
+
const details = toApprovePlanToolDetails(
|
|
210
|
+
validated,
|
|
211
|
+
outcome.response,
|
|
212
|
+
outcome.cancelled,
|
|
213
|
+
);
|
|
214
|
+
notifyPlanApproval(hooks, details, "approve_plan");
|
|
215
|
+
const text = formatApprovePlanResultText(
|
|
216
|
+
outcome.response,
|
|
217
|
+
outcome.cancelled,
|
|
218
|
+
);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text }],
|
|
221
|
+
details,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
renderCall(args, theme) {
|
|
225
|
+
return renderApprovePlanCall(args, theme);
|
|
226
|
+
},
|
|
227
|
+
renderResult(result, options, theme) {
|
|
228
|
+
return renderApprovePlanResult(result, options, theme);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
pi.registerTool({
|
|
233
|
+
name: "create_plan",
|
|
234
|
+
label: "Create Plan",
|
|
235
|
+
description:
|
|
236
|
+
"Write the approved PlanPacket to the canonical plan-packet.json for this harness run. Requires approve_plan Approve first. Do not use write/edit.",
|
|
237
|
+
promptSnippet: CREATE_PLAN_SNIPPET,
|
|
238
|
+
promptGuidelines: CREATE_PLAN_GUIDELINES,
|
|
239
|
+
parameters: CreatePlanParamsSchema,
|
|
240
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
241
|
+
const validated = validateApprovePlanParams(
|
|
242
|
+
params as ApprovePlanParams,
|
|
243
|
+
);
|
|
244
|
+
if (typeof validated === "string") {
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text", text: validated }],
|
|
247
|
+
details: { error: validated },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const projectRoot = hooks?.projectRoot ?? parentCtx.cwd;
|
|
251
|
+
const parentEntries = hooks?.getParentEntries?.() ?? [];
|
|
252
|
+
const subEntries = ctx.sessionManager.getEntries();
|
|
253
|
+
const result = await executeCreatePlan(validated.plan_packet, {
|
|
254
|
+
projectRoot,
|
|
255
|
+
getParentEntries: () => parentEntries,
|
|
256
|
+
getSubagentEntries: () => subEntries,
|
|
257
|
+
getParentRunContext: () => hooks?.getParentRunContext?.() ?? null,
|
|
258
|
+
onCommitted: (runCtx, packet, planPath) => {
|
|
259
|
+
hooks?.onPlanCommitted?.(runCtx, packet, planPath);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const text = formatCreatePlanResultText(result);
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text }],
|
|
265
|
+
details: result.ok
|
|
266
|
+
? {
|
|
267
|
+
plan_path: result.planPath,
|
|
268
|
+
plan_id: result.planId,
|
|
269
|
+
}
|
|
270
|
+
: { error: result.error },
|
|
271
|
+
isError: !result.ok,
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
renderCall(args, theme) {
|
|
275
|
+
const packet = (args as { plan_packet?: PlanPacketLike }).plan_packet;
|
|
276
|
+
const id = packet?.plan_id ?? "?";
|
|
277
|
+
return new Text(theme.fg("accent", `create_plan: ${id}`), 0, 0);
|
|
278
|
+
},
|
|
279
|
+
renderResult(result, _options, theme) {
|
|
280
|
+
const details = result.details as
|
|
281
|
+
| { plan_path?: string; error?: string }
|
|
282
|
+
| undefined;
|
|
283
|
+
if (details?.error) {
|
|
284
|
+
return new Text(
|
|
285
|
+
theme.fg("error", details.error ?? "create_plan failed"),
|
|
286
|
+
0,
|
|
287
|
+
0,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return new Text(
|
|
291
|
+
theme.fg(
|
|
292
|
+
"success",
|
|
293
|
+
`Wrote ${details?.plan_path ?? "plan-packet.json"}`,
|
|
294
|
+
),
|
|
295
|
+
0,
|
|
296
|
+
0,
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** @deprecated Use createParentHarnessUiBridgeFactory */
|
|
304
|
+
export function createParentAskUserBridgeFactory(
|
|
305
|
+
parentCtx: ExtensionContext,
|
|
306
|
+
agentType: string,
|
|
307
|
+
hooks?: ParentHarnessUiHooks,
|
|
308
|
+
): ((pi: ExtensionAPI) => void) | null {
|
|
309
|
+
return createParentHarnessUiBridgeFactory(parentCtx, agentType, hooks);
|
|
310
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
appendPlanApprovalIfNew,
|
|
4
|
+
getLatestRunContext,
|
|
5
|
+
type HarnessRunContext,
|
|
6
|
+
nowIso,
|
|
7
|
+
type PlanUserApproval,
|
|
8
|
+
planPacketSummary,
|
|
9
|
+
} from "../../../lib/harness-run-context.js";
|
|
10
|
+
import type { ParentHarnessUiHooks } from "./parent-harness-ui-bridge.js";
|
|
11
|
+
|
|
12
|
+
function persistRunContext(pi: ExtensionAPI, runCtx: HarnessRunContext): void {
|
|
13
|
+
pi.appendEntry("harness-run-context", runCtx);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createParentHarnessUiHooks(
|
|
17
|
+
pi: ExtensionAPI,
|
|
18
|
+
getParentEntries: () => unknown[],
|
|
19
|
+
projectRoot: string,
|
|
20
|
+
): ParentHarnessUiHooks {
|
|
21
|
+
return {
|
|
22
|
+
projectRoot,
|
|
23
|
+
getParentEntries,
|
|
24
|
+
getParentRunContext: () => getLatestRunContext(getParentEntries()),
|
|
25
|
+
appendPlanDraft: (draft) => {
|
|
26
|
+
const planId = String(draft.plan_packet.plan_id ?? "plan");
|
|
27
|
+
const summary =
|
|
28
|
+
draft.human_summary?.trim() || `Plan ${planId} — pending your approval`;
|
|
29
|
+
pi.sendMessage({
|
|
30
|
+
customType: "harness-plan-draft",
|
|
31
|
+
content: summary,
|
|
32
|
+
display: true,
|
|
33
|
+
details: {
|
|
34
|
+
schema_version: "1.0.0",
|
|
35
|
+
plan_packet: draft.plan_packet,
|
|
36
|
+
human_summary: draft.human_summary ?? null,
|
|
37
|
+
shown_at: nowIso(),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
onPlanApproval: (approval: PlanUserApproval) => {
|
|
42
|
+
const entries = getParentEntries();
|
|
43
|
+
const runCtx = getLatestRunContext(entries);
|
|
44
|
+
appendPlanApprovalIfNew(
|
|
45
|
+
(type, data) => pi.appendEntry(type, data),
|
|
46
|
+
entries,
|
|
47
|
+
approval,
|
|
48
|
+
runCtx,
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
onPlanCommitted: (runCtx, packet, planPath) => {
|
|
52
|
+
persistRunContext(pi, runCtx);
|
|
53
|
+
pi.appendEntry(
|
|
54
|
+
"harness-plan-packet",
|
|
55
|
+
planPacketSummary(packet, planPath, "ready"),
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -41,5 +41,14 @@ export function evaluateSubagentToolCall(
|
|
|
41
41
|
reason: `Tool "ask_user" is not available for ${agentType ?? "this agent"} (orchestrator-only).`,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
if (toolName === "approve_plan" || toolName === "create_plan") {
|
|
45
|
+
if (agentType === "harness/planner") {
|
|
46
|
+
return { action: "allow" };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
action: "block",
|
|
50
|
+
reason: `Tool "${toolName}" is only available for harness/planner.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
44
53
|
return { action: "allow" };
|
|
45
54
|
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ExtensionAPI,
|
|
14
14
|
ExtensionContext,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ParentHarnessUiHooks } from "../parent-harness-ui-bridge.js";
|
|
16
17
|
import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
|
|
17
18
|
import type {
|
|
18
19
|
AgentInvocation,
|
|
@@ -84,6 +85,8 @@ interface SpawnOptions {
|
|
|
84
85
|
onCompaction?: (info: CompactionInfo) => void;
|
|
85
86
|
/** Spawn context (blackboard injection) for subagent system prompt. */
|
|
86
87
|
systemPromptAppendix?: string;
|
|
88
|
+
/** Parent UI hooks for plan approval sync and draft transcript. */
|
|
89
|
+
parentHarnessUiHooks?: ParentHarnessUiHooks;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
export class AgentManager {
|
|
@@ -253,6 +256,7 @@ export class AgentManager {
|
|
|
253
256
|
},
|
|
254
257
|
systemPromptAppendix: options.systemPromptAppendix,
|
|
255
258
|
parentExtensionContext: ctx,
|
|
259
|
+
parentHarnessUiHooks: options.parentHarnessUiHooks,
|
|
256
260
|
})
|
|
257
261
|
.then(({ responseText, session, aborted, steered }) => {
|
|
258
262
|
// Don't overwrite status if externally stopped via abort()
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
SettingsManager,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { evaluateHarnessSubagentToolCall } from "../harness-subagent-policy.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createParentHarnessUiBridgeFactory,
|
|
23
|
+
type ParentHarnessUiHooks,
|
|
24
|
+
} from "../parent-harness-ui-bridge.js";
|
|
22
25
|
import {
|
|
23
26
|
getAgentConfig,
|
|
24
27
|
getConfig,
|
|
@@ -152,8 +155,10 @@ export interface RunOptions {
|
|
|
152
155
|
}) => void;
|
|
153
156
|
/** Blackboard or other spawn context appended to the subagent system prompt. */
|
|
154
157
|
systemPromptAppendix?: string;
|
|
155
|
-
/** Parent session context — used to bridge ask_user UI into subagents. */
|
|
158
|
+
/** Parent session context — used to bridge ask_user / approve_plan UI into subagents. */
|
|
156
159
|
parentExtensionContext?: ExtensionContext;
|
|
160
|
+
/** Parent-session hooks (plan draft transcript, approval sync). */
|
|
161
|
+
parentHarnessUiHooks?: ParentHarnessUiHooks;
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
export interface RunResult {
|
|
@@ -331,11 +336,15 @@ export async function runAgent(
|
|
|
331
336
|
: systemPrompt;
|
|
332
337
|
|
|
333
338
|
const extensionFactories: Array<(pi: ExtensionAPI) => void> = [];
|
|
334
|
-
const
|
|
335
|
-
?
|
|
339
|
+
const harnessUiBridge = options.parentExtensionContext
|
|
340
|
+
? createParentHarnessUiBridgeFactory(
|
|
341
|
+
options.parentExtensionContext,
|
|
342
|
+
type,
|
|
343
|
+
options.parentHarnessUiHooks,
|
|
344
|
+
)
|
|
336
345
|
: null;
|
|
337
|
-
if (
|
|
338
|
-
extensionFactories.push(
|
|
346
|
+
if (harnessUiBridge) {
|
|
347
|
+
extensionFactories.push(harnessUiBridge);
|
|
339
348
|
}
|
|
340
349
|
extensionFactories.push((pi) => {
|
|
341
350
|
pi.on("tool_call", (event) => {
|
|
@@ -410,7 +419,14 @@ export async function runAgent(
|
|
|
410
419
|
const filterTools = (names: string[]) =>
|
|
411
420
|
names.filter((t) => {
|
|
412
421
|
if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
|
|
413
|
-
if (t === "ask_user" &&
|
|
422
|
+
if (t === "ask_user" && harnessUiBridge) return true;
|
|
423
|
+
if (
|
|
424
|
+
(t === "approve_plan" || t === "create_plan") &&
|
|
425
|
+
harnessUiBridge &&
|
|
426
|
+
type === "harness/planner"
|
|
427
|
+
) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
414
430
|
if (disallowedSet?.has(t)) return false;
|
|
415
431
|
if (builtinToolNameSet.has(t)) return true;
|
|
416
432
|
if (extensions === false) return false;
|
|
@@ -425,7 +441,14 @@ export async function runAgent(
|
|
|
425
441
|
session.setActiveToolsByName(activeTools);
|
|
426
442
|
} else {
|
|
427
443
|
const fallback = toolNames.filter((t) => {
|
|
428
|
-
if (t === "ask_user" &&
|
|
444
|
+
if (t === "ask_user" && harnessUiBridge) return true;
|
|
445
|
+
if (
|
|
446
|
+
(t === "approve_plan" || t === "create_plan") &&
|
|
447
|
+
harnessUiBridge &&
|
|
448
|
+
type === "harness/planner"
|
|
449
|
+
) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
429
452
|
return !disallowedSet?.has(t);
|
|
430
453
|
});
|
|
431
454
|
session.setActiveToolsByName(fallback);
|
|
@@ -444,10 +467,14 @@ export async function runAgent(
|
|
|
444
467
|
},
|
|
445
468
|
});
|
|
446
469
|
|
|
447
|
-
if (
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
470
|
+
if (harnessUiBridge) {
|
|
471
|
+
const withHarnessUi = new Set(session.getActiveToolNames());
|
|
472
|
+
withHarnessUi.add("ask_user");
|
|
473
|
+
if (type === "harness/planner") {
|
|
474
|
+
withHarnessUi.add("approve_plan");
|
|
475
|
+
withHarnessUi.add("create_plan");
|
|
476
|
+
}
|
|
477
|
+
session.setActiveToolsByName([...withHarnessUi]);
|
|
451
478
|
}
|
|
452
479
|
|
|
453
480
|
options.onSessionCreated?.(session);
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
import { Text } from "@earendil-works/pi-tui";
|
|
19
19
|
import { Type } from "@sinclair/typebox";
|
|
20
20
|
import {
|
|
21
|
-
extractPlanApprovalsFromEntries,
|
|
22
21
|
getLatestRunContext,
|
|
22
|
+
syncPlannerApprovalsToParent,
|
|
23
23
|
} from "../../../../lib/harness-run-context.js";
|
|
24
24
|
import { getDriftReport } from "../agent-manifest.js";
|
|
25
25
|
import { Blackboard } from "../blackboard.js";
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
buildBlackboardContextInjection,
|
|
28
28
|
registerBlackboardTool,
|
|
29
29
|
} from "../blackboard-tool.js";
|
|
30
|
+
import type { ParentHarnessUiHooks } from "../parent-harness-ui-bridge.js";
|
|
31
|
+
import { createParentHarnessUiHooks } from "../parent-harness-ui-hooks.js";
|
|
30
32
|
import { AgentManager } from "./agent-manager.js";
|
|
31
33
|
import {
|
|
32
34
|
getAgentConversation,
|
|
@@ -650,6 +652,7 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
|
|
|
650
652
|
|
|
651
653
|
// --- Cross-extension RPC via pi.events ---
|
|
652
654
|
let currentCtx: ExtensionContext | undefined;
|
|
655
|
+
let parentHarnessUiHooks: ParentHarnessUiHooks | undefined;
|
|
653
656
|
|
|
654
657
|
// ---- Subagent scheduler ----
|
|
655
658
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
@@ -678,6 +681,11 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
|
|
|
678
681
|
// Capture ctx from session_start for RPC spawn handler + start the scheduler.
|
|
679
682
|
pi.on("session_start", async (_event, ctx) => {
|
|
680
683
|
currentCtx = ctx;
|
|
684
|
+
parentHarnessUiHooks = createParentHarnessUiHooks(
|
|
685
|
+
pi,
|
|
686
|
+
() => ctx.sessionManager.getEntries(),
|
|
687
|
+
ctx.cwd,
|
|
688
|
+
);
|
|
681
689
|
manager.clearCompleted();
|
|
682
690
|
if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
|
|
683
691
|
|
|
@@ -751,7 +759,9 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
|
|
|
751
759
|
});
|
|
752
760
|
|
|
753
761
|
// Live widget: show running agents above editor
|
|
754
|
-
const widget = new AgentWidget(manager, agentActivity)
|
|
762
|
+
const widget = new AgentWidget(manager, agentActivity, () => {
|
|
763
|
+
pi.events.emit("subagents:agents-widget-mounted", {});
|
|
764
|
+
});
|
|
755
765
|
|
|
756
766
|
// ---- Join mode configuration ----
|
|
757
767
|
let defaultJoinMode: JoinMode = "smart";
|
|
@@ -1338,6 +1348,7 @@ Guidelines:
|
|
|
1338
1348
|
isolation,
|
|
1339
1349
|
invocation: agentInvocation,
|
|
1340
1350
|
systemPromptAppendix,
|
|
1351
|
+
parentHarnessUiHooks,
|
|
1341
1352
|
...bgCallbacks,
|
|
1342
1353
|
});
|
|
1343
1354
|
} catch (err) {
|
|
@@ -1481,6 +1492,7 @@ Guidelines:
|
|
|
1481
1492
|
invocation: agentInvocation,
|
|
1482
1493
|
systemPromptAppendix,
|
|
1483
1494
|
signal,
|
|
1495
|
+
parentHarnessUiHooks,
|
|
1484
1496
|
...fgCallbacks,
|
|
1485
1497
|
},
|
|
1486
1498
|
);
|
|
@@ -1491,6 +1503,24 @@ Guidelines:
|
|
|
1491
1503
|
|
|
1492
1504
|
clearInterval(spinnerInterval);
|
|
1493
1505
|
|
|
1506
|
+
if (
|
|
1507
|
+
subagentType === "harness/planner" &&
|
|
1508
|
+
record.session &&
|
|
1509
|
+
record.status !== "running" &&
|
|
1510
|
+
record.status !== "queued"
|
|
1511
|
+
) {
|
|
1512
|
+
const parentEntries = ctx.sessionManager.getEntries();
|
|
1513
|
+
const runCtx = getLatestRunContext(parentEntries);
|
|
1514
|
+
if (runCtx) {
|
|
1515
|
+
syncPlannerApprovalsToParent(
|
|
1516
|
+
(type, data) => pi.appendEntry(type, data),
|
|
1517
|
+
parentEntries,
|
|
1518
|
+
record.session.sessionManager.getEntries(),
|
|
1519
|
+
runCtx,
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1494
1524
|
// Clean up foreground agent from widget
|
|
1495
1525
|
if (fgId) {
|
|
1496
1526
|
agentActivity.delete(fgId);
|
|
@@ -1607,16 +1637,12 @@ Guidelines:
|
|
|
1607
1637
|
const parentEntries = _ctx.sessionManager.getEntries();
|
|
1608
1638
|
const runCtx = getLatestRunContext(parentEntries);
|
|
1609
1639
|
if (runCtx) {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
approved_at: approval.approved_at,
|
|
1617
|
-
source: "ask_user",
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1640
|
+
syncPlannerApprovalsToParent(
|
|
1641
|
+
(type, data) => pi.appendEntry(type, data),
|
|
1642
|
+
parentEntries,
|
|
1643
|
+
record.session.sessionManager.getEntries(),
|
|
1644
|
+
runCtx,
|
|
1645
|
+
);
|
|
1620
1646
|
}
|
|
1621
1647
|
}
|
|
1622
1648
|
|
|
@@ -264,6 +264,7 @@ export class AgentWidget {
|
|
|
264
264
|
constructor(
|
|
265
265
|
private manager: AgentManager,
|
|
266
266
|
private agentActivity: Map<string, AgentActivity>,
|
|
267
|
+
private onWidgetRegistered?: () => void,
|
|
267
268
|
) {}
|
|
268
269
|
|
|
269
270
|
/** Set the UI context (grabbed from first tool execution). */
|
|
@@ -615,6 +616,7 @@ export class AgentWidget {
|
|
|
615
616
|
{ placement: "aboveEditor" },
|
|
616
617
|
);
|
|
617
618
|
this.widgetRegistered = true;
|
|
619
|
+
this.onWidgetRegistered?.();
|
|
618
620
|
} else {
|
|
619
621
|
// Widget already registered — just request a re-render of existing components.
|
|
620
622
|
this.tui?.requestRender();
|