ultimate-pi 0.9.1 → 0.10.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/.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-plan-approval.ts +140 -0
- package/.pi/extensions/harness-run-context.ts +29 -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 +306 -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 +35 -11
- package/.pi/extensions/lib/plan-approval/create-plan.ts +131 -0
- package/.pi/extensions/lib/plan-approval/dialog.ts +207 -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 +6 -6
- package/CHANGELOG.md +6 -0
- package/package.json +3 -3
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
} else {
|
|
203
|
+
outcome = await runPlanApprovalFallback(parentCtx.ui, validated);
|
|
204
|
+
}
|
|
205
|
+
const details = toApprovePlanToolDetails(
|
|
206
|
+
validated,
|
|
207
|
+
outcome.response,
|
|
208
|
+
outcome.cancelled,
|
|
209
|
+
);
|
|
210
|
+
notifyPlanApproval(hooks, details, "approve_plan");
|
|
211
|
+
const text = formatApprovePlanResultText(
|
|
212
|
+
outcome.response,
|
|
213
|
+
outcome.cancelled,
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text }],
|
|
217
|
+
details,
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
renderCall(args, theme) {
|
|
221
|
+
return renderApprovePlanCall(args, theme);
|
|
222
|
+
},
|
|
223
|
+
renderResult(result, options, theme) {
|
|
224
|
+
return renderApprovePlanResult(result, options, theme);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.registerTool({
|
|
229
|
+
name: "create_plan",
|
|
230
|
+
label: "Create Plan",
|
|
231
|
+
description:
|
|
232
|
+
"Write the approved PlanPacket to the canonical plan-packet.json for this harness run. Requires approve_plan Approve first. Do not use write/edit.",
|
|
233
|
+
promptSnippet: CREATE_PLAN_SNIPPET,
|
|
234
|
+
promptGuidelines: CREATE_PLAN_GUIDELINES,
|
|
235
|
+
parameters: CreatePlanParamsSchema,
|
|
236
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
237
|
+
const validated = validateApprovePlanParams(
|
|
238
|
+
params as ApprovePlanParams,
|
|
239
|
+
);
|
|
240
|
+
if (typeof validated === "string") {
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: validated }],
|
|
243
|
+
details: { error: validated },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const projectRoot = hooks?.projectRoot ?? parentCtx.cwd;
|
|
247
|
+
const parentEntries = hooks?.getParentEntries?.() ?? [];
|
|
248
|
+
const subEntries = ctx.sessionManager.getEntries();
|
|
249
|
+
const result = await executeCreatePlan(validated.plan_packet, {
|
|
250
|
+
projectRoot,
|
|
251
|
+
getParentEntries: () => parentEntries,
|
|
252
|
+
getSubagentEntries: () => subEntries,
|
|
253
|
+
getParentRunContext: () => hooks?.getParentRunContext?.() ?? null,
|
|
254
|
+
onCommitted: (runCtx, packet, planPath) => {
|
|
255
|
+
hooks?.onPlanCommitted?.(runCtx, packet, planPath);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const text = formatCreatePlanResultText(result);
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: "text", text }],
|
|
261
|
+
details: result.ok
|
|
262
|
+
? {
|
|
263
|
+
plan_path: result.planPath,
|
|
264
|
+
plan_id: result.planId,
|
|
265
|
+
}
|
|
266
|
+
: { error: result.error },
|
|
267
|
+
isError: !result.ok,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
renderCall(args, theme) {
|
|
271
|
+
const packet = (args as { plan_packet?: PlanPacketLike }).plan_packet;
|
|
272
|
+
const id = packet?.plan_id ?? "?";
|
|
273
|
+
return new Text(theme.fg("accent", `create_plan: ${id}`), 0, 0);
|
|
274
|
+
},
|
|
275
|
+
renderResult(result, _options, theme) {
|
|
276
|
+
const details = result.details as
|
|
277
|
+
| { plan_path?: string; error?: string }
|
|
278
|
+
| undefined;
|
|
279
|
+
if (details?.error) {
|
|
280
|
+
return new Text(
|
|
281
|
+
theme.fg("error", details.error ?? "create_plan failed"),
|
|
282
|
+
0,
|
|
283
|
+
0,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return new Text(
|
|
287
|
+
theme.fg(
|
|
288
|
+
"success",
|
|
289
|
+
`Wrote ${details?.plan_path ?? "plan-packet.json"}`,
|
|
290
|
+
),
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @deprecated Use createParentHarnessUiBridgeFactory */
|
|
300
|
+
export function createParentAskUserBridgeFactory(
|
|
301
|
+
parentCtx: ExtensionContext,
|
|
302
|
+
agentType: string,
|
|
303
|
+
hooks?: ParentHarnessUiHooks,
|
|
304
|
+
): ((pi: ExtensionAPI) => void) | null {
|
|
305
|
+
return createParentHarnessUiBridgeFactory(parentCtx, agentType, hooks);
|
|
306
|
+
}
|
|
@@ -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
|
|
|
@@ -1338,6 +1346,7 @@ Guidelines:
|
|
|
1338
1346
|
isolation,
|
|
1339
1347
|
invocation: agentInvocation,
|
|
1340
1348
|
systemPromptAppendix,
|
|
1349
|
+
parentHarnessUiHooks,
|
|
1341
1350
|
...bgCallbacks,
|
|
1342
1351
|
});
|
|
1343
1352
|
} catch (err) {
|
|
@@ -1481,6 +1490,7 @@ Guidelines:
|
|
|
1481
1490
|
invocation: agentInvocation,
|
|
1482
1491
|
systemPromptAppendix,
|
|
1483
1492
|
signal,
|
|
1493
|
+
parentHarnessUiHooks,
|
|
1484
1494
|
...fgCallbacks,
|
|
1485
1495
|
},
|
|
1486
1496
|
);
|
|
@@ -1491,6 +1501,24 @@ Guidelines:
|
|
|
1491
1501
|
|
|
1492
1502
|
clearInterval(spinnerInterval);
|
|
1493
1503
|
|
|
1504
|
+
if (
|
|
1505
|
+
subagentType === "harness/planner" &&
|
|
1506
|
+
record.session &&
|
|
1507
|
+
record.status !== "running" &&
|
|
1508
|
+
record.status !== "queued"
|
|
1509
|
+
) {
|
|
1510
|
+
const parentEntries = ctx.sessionManager.getEntries();
|
|
1511
|
+
const runCtx = getLatestRunContext(parentEntries);
|
|
1512
|
+
if (runCtx) {
|
|
1513
|
+
syncPlannerApprovalsToParent(
|
|
1514
|
+
(type, data) => pi.appendEntry(type, data),
|
|
1515
|
+
parentEntries,
|
|
1516
|
+
record.session.sessionManager.getEntries(),
|
|
1517
|
+
runCtx,
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1494
1522
|
// Clean up foreground agent from widget
|
|
1495
1523
|
if (fgId) {
|
|
1496
1524
|
agentActivity.delete(fgId);
|
|
@@ -1607,16 +1635,12 @@ Guidelines:
|
|
|
1607
1635
|
const parentEntries = _ctx.sessionManager.getEntries();
|
|
1608
1636
|
const runCtx = getLatestRunContext(parentEntries);
|
|
1609
1637
|
if (runCtx) {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
approved_at: approval.approved_at,
|
|
1617
|
-
source: "ask_user",
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1638
|
+
syncPlannerApprovalsToParent(
|
|
1639
|
+
(type, data) => pi.appendEntry(type, data),
|
|
1640
|
+
parentEntries,
|
|
1641
|
+
record.session.sessionManager.getEntries(),
|
|
1642
|
+
runCtx,
|
|
1643
|
+
);
|
|
1620
1644
|
}
|
|
1621
1645
|
}
|
|
1622
1646
|
|