pi-crew 0.9.66 → 0.9.68
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 +109 -0
- package/README.md +2 -3
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +4451 -4092
- package/package.json +12 -8
- package/scripts/analyze-run.mjs +1333 -0
- package/scripts/resource-sampler.mjs +482 -0
- package/skills/real-test-pi-crew/SKILL.md +10 -9
- package/src/agents/agent-config.ts +35 -14
- package/src/config/role-tools.ts +2 -2
- package/src/extension/knowledge-injection.ts +17 -0
- package/src/extension/pi-api.ts +0 -16
- package/src/extension/team-tool/api/agent-control.ts +358 -0
- package/src/extension/team-tool/api/handler-context.ts +57 -0
- package/src/extension/team-tool/api/heartbeat.ts +75 -0
- package/src/extension/team-tool/api/mailbox.ts +242 -0
- package/src/extension/team-tool/api/plan-approval.ts +190 -0
- package/src/extension/team-tool/api/read.ts +443 -0
- package/src/extension/team-tool/api/task-claims.ts +207 -0
- package/src/extension/team-tool/api.ts +56 -1301
- package/src/extension/team-tool/cancel.ts +15 -4
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +4 -4
- package/src/extension/team-tool/lifecycle-actions.ts +5 -5
- package/src/extension/team-tool/parallel-dispatch.ts +2 -2
- package/src/extension/team-tool/respond.ts +11 -3
- package/src/extension/team-tool/run-intent.ts +357 -0
- package/src/extension/team-tool/run.ts +8 -285
- package/src/extension/team-tool/status.ts +56 -21
- package/src/extension/team-tool.ts +10 -8
- package/src/prompt/scratchpad-lifecycle.ts +80 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
- package/src/runtime/child-pi/child-pi.ts +22 -144
- package/src/runtime/child-pi/mock-fixtures.ts +171 -0
- package/src/runtime/crew-agent-records.ts +18 -1
- package/src/runtime/scratchpad/README.md +11 -0
- package/src/runtime/scratchpad/guest.ts +157 -7
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- package/src/runtime/supervisor-contact.ts +0 -16
- package/src/runtime/task-runner/child-executor.ts +1 -0
- package/src/runtime/task-runner/state-helpers.ts +9 -1
- package/src/runtime/team-runner.ts +39 -2
- package/src/state/contracts.ts +3 -0
- package/src/state/event-log/event-log.ts +19 -27
- package/src/state/gitignore-manager.ts +61 -7
- package/src/utils/glob-match.ts +29 -0
- package/types/dwf.d.ts +1 -1
- package/src/runtime/scratchpad/snapshot-hmac.ts +0 -161
- package/src/types/new-api-types.ts +0 -35
|
@@ -301,6 +301,7 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
301
301
|
content = `${content.slice(0, MAX_KNOWLEDGE_HEAD_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_HEAD_BYTES} bytes (head shown). Full file: ${p} — use the \`read\` tool if you need sections beyond the head. -->`;
|
|
302
302
|
}
|
|
303
303
|
knowledgeCache.set(p, { key: cacheKey, content });
|
|
304
|
+
enforceKnowledgeCacheCap(knowledgeCache);
|
|
304
305
|
return content;
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -325,6 +326,7 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
325
326
|
conventions: parsed.conventions,
|
|
326
327
|
sessionLog: parsed.sessionLog,
|
|
327
328
|
});
|
|
329
|
+
enforceKnowledgeCacheCap(sectionCache);
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
const queryText = [query.goal, query.taskText].filter(Boolean).join(" \n ");
|
|
@@ -382,6 +384,21 @@ interface CachedSections {
|
|
|
382
384
|
}
|
|
383
385
|
const sectionCache = new Map<string, CachedSections>();
|
|
384
386
|
|
|
387
|
+
// H6 (2026-08-10): FIFO cap — mirrors the discover-* caches
|
|
388
|
+
// (TEAM_DISCOVERY_MAX_ENTRIES = 32, WORKFLOW_DISCOVERY_MAX_ENTRIES = 32).
|
|
389
|
+
// Without this, the caches grow with every distinct project root seen by the
|
|
390
|
+
// process; each entry can be KB-scale (full knowledge.md content + parsed
|
|
391
|
+
// sections). Eviction is safe: a cache miss falls through to a fresh
|
|
392
|
+
// readFileSync + parse, same as the first call for that path.
|
|
393
|
+
const KNOWLEDGE_CACHE_MAX_ENTRIES = 64;
|
|
394
|
+
|
|
395
|
+
function enforceKnowledgeCacheCap(cache: Map<string, unknown>): void {
|
|
396
|
+
if (cache.size > KNOWLEDGE_CACHE_MAX_ENTRIES) {
|
|
397
|
+
const oldest = cache.keys().next().value;
|
|
398
|
+
if (oldest !== undefined) cache.delete(oldest);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
385
402
|
/** Head cap for the no-query (legacy / main-session) path. */
|
|
386
403
|
const MAX_KNOWLEDGE_HEAD_BYTES = 2_000;
|
|
387
404
|
|
package/src/extension/pi-api.ts
CHANGED
|
@@ -38,19 +38,3 @@ export type {
|
|
|
38
38
|
} from "@earendil-works/pi-coding-agent";
|
|
39
39
|
|
|
40
40
|
export { createBashTool, defineTool } from "@earendil-works/pi-coding-agent";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @deprecated Drift detector removed in Phase 5 follow-up to H1.
|
|
44
|
-
*
|
|
45
|
-
* History: this constant was meant to surface version-drift between
|
|
46
|
-
* dev-time type-check and runtime peer-dep version. pi-crew declares
|
|
47
|
-
* peer deps as `*` so the runtime version is set by whatever host pi
|
|
48
|
-
* install the user has — pinning this constant to a specific devDep
|
|
49
|
-
* range was a false invariant that did not reflect how extensions
|
|
50
|
-
* actually consume peer packages.
|
|
51
|
-
*
|
|
52
|
-
* Kept exported (now `"0.0.0-unset"`) only for any downstream consumers
|
|
53
|
-
* that may import it via re-exports. No diagnostic uses it anymore.
|
|
54
|
-
* Will be removed in a future major version.
|
|
55
|
-
*/
|
|
56
|
-
export const BUILT_AGAINST_PI_VERSION = "0.0.0-unset";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted `api` operation handlers — live-agent control group (H3 phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
|
|
5
|
+
* byte-identical to the inline `if` blocks they replace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readCrewAgents } from "../../../runtime/crew-agent-records.ts";
|
|
9
|
+
import { appendLiveAgentControlRequest } from "../../../runtime/live-session/live-agent-control.ts";
|
|
10
|
+
import {
|
|
11
|
+
followUpLiveAgent,
|
|
12
|
+
getLiveAgent,
|
|
13
|
+
listActiveLiveAgents,
|
|
14
|
+
resumeLiveAgent,
|
|
15
|
+
steerLiveAgent,
|
|
16
|
+
stopLiveAgent,
|
|
17
|
+
} from "../../../runtime/live-session/live-agent-manager.ts";
|
|
18
|
+
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../../runtime/live-session/live-control-realtime.ts";
|
|
19
|
+
import {
|
|
20
|
+
appendFollowUpMessage,
|
|
21
|
+
appendFollowUpMessageAsync,
|
|
22
|
+
appendSteeringMessage,
|
|
23
|
+
appendSteeringMessageAsync,
|
|
24
|
+
} from "../../../state/coordination/mailbox.ts";
|
|
25
|
+
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
26
|
+
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
27
|
+
|
|
28
|
+
export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
|
|
29
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
30
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
31
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
32
|
+
if (!agent)
|
|
33
|
+
return result(
|
|
34
|
+
paramRequired(
|
|
35
|
+
"API nudge-agent",
|
|
36
|
+
"config.agentId matching an agent id or task id",
|
|
37
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'nudge-agent', agentId: 'agent-1' } }",
|
|
38
|
+
),
|
|
39
|
+
{
|
|
40
|
+
action: "api",
|
|
41
|
+
status: "error",
|
|
42
|
+
runId: loaded.manifest.runId,
|
|
43
|
+
},
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
const messageText =
|
|
47
|
+
typeof cfg.message === "string" && cfg.message.trim()
|
|
48
|
+
? cfg.message.trim()
|
|
49
|
+
: "Please report your current status, blocker, or smallest next step.";
|
|
50
|
+
const message = appendSteeringMessage(loaded.manifest, {
|
|
51
|
+
taskId: agent.taskId,
|
|
52
|
+
to: agent.taskId,
|
|
53
|
+
body: messageText,
|
|
54
|
+
priority: "normal",
|
|
55
|
+
data: { source: "nudge-agent" },
|
|
56
|
+
});
|
|
57
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
58
|
+
type: "agent.nudged",
|
|
59
|
+
runId: loaded.manifest.runId,
|
|
60
|
+
taskId: agent.taskId,
|
|
61
|
+
message: messageText,
|
|
62
|
+
data: { agentId: agent.id, mailboxMessageId: message.id },
|
|
63
|
+
});
|
|
64
|
+
ctx.events?.emit?.("crew.mailbox.message", {
|
|
65
|
+
runId: loaded.manifest.runId,
|
|
66
|
+
id: message.id,
|
|
67
|
+
direction: message.direction,
|
|
68
|
+
from: message.from,
|
|
69
|
+
to: message.to,
|
|
70
|
+
taskId: message.taskId,
|
|
71
|
+
source: "nudge-agent",
|
|
72
|
+
});
|
|
73
|
+
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), {
|
|
74
|
+
action: "api",
|
|
75
|
+
status: "ok",
|
|
76
|
+
runId: loaded.manifest.runId,
|
|
77
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const handleListLiveAgents: ApiOperationHandler = (hctx) => {
|
|
82
|
+
const { loaded, result } = hctx;
|
|
83
|
+
return result(
|
|
84
|
+
JSON.stringify(
|
|
85
|
+
listActiveLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId),
|
|
86
|
+
null,
|
|
87
|
+
2,
|
|
88
|
+
),
|
|
89
|
+
{
|
|
90
|
+
action: "api",
|
|
91
|
+
status: "ok",
|
|
92
|
+
runId: loaded.manifest.runId,
|
|
93
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handles steer-agent / follow-up-agent / stop-agent / resume-agent /
|
|
100
|
+
* interrupt-agent — the live-agent control cluster (shared body in the
|
|
101
|
+
* original handleApi; interrupt falls through to stop).
|
|
102
|
+
*/
|
|
103
|
+
export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
|
|
104
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
105
|
+
const operation = typeof cfg.operation === "string" ? cfg.operation : "";
|
|
106
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
107
|
+
if (!agentId)
|
|
108
|
+
return result(
|
|
109
|
+
paramRequired(
|
|
110
|
+
`API ${operation}`,
|
|
111
|
+
"config.agentId",
|
|
112
|
+
`{ action: 'api', runId: 'team_...', config: { operation: '${operation}', agentId: 'agent-1' } }`,
|
|
113
|
+
),
|
|
114
|
+
{
|
|
115
|
+
action: "api",
|
|
116
|
+
status: "error",
|
|
117
|
+
runId: loaded.manifest.runId,
|
|
118
|
+
},
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
122
|
+
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
123
|
+
try {
|
|
124
|
+
const live = getLiveAgent(agentId);
|
|
125
|
+
if (live && live.runId !== loaded.manifest.runId)
|
|
126
|
+
return result(
|
|
127
|
+
`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`,
|
|
128
|
+
{
|
|
129
|
+
action: "api",
|
|
130
|
+
status: "error",
|
|
131
|
+
runId: loaded.manifest.runId,
|
|
132
|
+
},
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
if (live && live.workspaceId !== loaded.manifest.cwd)
|
|
136
|
+
return result(
|
|
137
|
+
`Live agent '${agentId}' does not belong to workspace ${loaded.manifest.cwd}.`,
|
|
138
|
+
{
|
|
139
|
+
action: "api",
|
|
140
|
+
status: "error",
|
|
141
|
+
runId: loaded.manifest.runId,
|
|
142
|
+
},
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
if (!live && (operation === "steer-agent" || operation === "follow-up-agent"))
|
|
146
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
147
|
+
const liveTaskId = live?.taskId;
|
|
148
|
+
if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId)
|
|
149
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
150
|
+
const targetTaskId = liveTaskId ?? agentId;
|
|
151
|
+
if (operation === "steer-agent") {
|
|
152
|
+
const text = message ?? "Please report current status and wrap up if possible.";
|
|
153
|
+
const realtime = await steerLiveAgent(agentId, text);
|
|
154
|
+
const mailboxMessage = await appendSteeringMessageAsync(loaded.manifest, {
|
|
155
|
+
taskId: targetTaskId,
|
|
156
|
+
body: text,
|
|
157
|
+
status: "delivered",
|
|
158
|
+
data: { source: "steer-agent", realtime: true },
|
|
159
|
+
});
|
|
160
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
161
|
+
action: "api",
|
|
162
|
+
status: "ok",
|
|
163
|
+
runId: loaded.manifest.runId,
|
|
164
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (operation === "follow-up-agent") {
|
|
168
|
+
if (!prompt)
|
|
169
|
+
return result(
|
|
170
|
+
paramRequired(
|
|
171
|
+
"API follow-up-agent",
|
|
172
|
+
"config.prompt or config.message",
|
|
173
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
174
|
+
),
|
|
175
|
+
{
|
|
176
|
+
action: "api",
|
|
177
|
+
status: "error",
|
|
178
|
+
runId: loaded.manifest.runId,
|
|
179
|
+
},
|
|
180
|
+
true,
|
|
181
|
+
);
|
|
182
|
+
const realtime = await followUpLiveAgent(agentId, prompt);
|
|
183
|
+
const mailboxMessage = await appendFollowUpMessageAsync(loaded.manifest, {
|
|
184
|
+
taskId: targetTaskId,
|
|
185
|
+
body: prompt,
|
|
186
|
+
status: "delivered",
|
|
187
|
+
data: { source: "follow-up-agent", realtime: true },
|
|
188
|
+
});
|
|
189
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
190
|
+
action: "api",
|
|
191
|
+
status: "ok",
|
|
192
|
+
runId: loaded.manifest.runId,
|
|
193
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (operation === "resume-agent") {
|
|
197
|
+
if (!prompt)
|
|
198
|
+
return result(
|
|
199
|
+
paramRequired(
|
|
200
|
+
"API resume-agent",
|
|
201
|
+
"config.prompt or config.message",
|
|
202
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
203
|
+
),
|
|
204
|
+
{
|
|
205
|
+
action: "api",
|
|
206
|
+
status: "error",
|
|
207
|
+
runId: loaded.manifest.runId,
|
|
208
|
+
},
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
211
|
+
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), {
|
|
212
|
+
action: "api",
|
|
213
|
+
status: "ok",
|
|
214
|
+
runId: loaded.manifest.runId,
|
|
215
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), {
|
|
219
|
+
action: "api",
|
|
220
|
+
status: "ok",
|
|
221
|
+
runId: loaded.manifest.runId,
|
|
222
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
226
|
+
if (!agent) {
|
|
227
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
228
|
+
return result(
|
|
229
|
+
err,
|
|
230
|
+
{
|
|
231
|
+
action: "api",
|
|
232
|
+
status: "error",
|
|
233
|
+
runId: loaded.manifest.runId,
|
|
234
|
+
},
|
|
235
|
+
true,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
239
|
+
if (!task)
|
|
240
|
+
return result(
|
|
241
|
+
`API ${operation} agent '${agentId}' does not match a run task.`,
|
|
242
|
+
{
|
|
243
|
+
action: "api",
|
|
244
|
+
status: "error",
|
|
245
|
+
runId: loaded.manifest.runId,
|
|
246
|
+
},
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
if (operation === "resume-agent" && !prompt)
|
|
250
|
+
return result(
|
|
251
|
+
paramRequired(
|
|
252
|
+
"API resume-agent",
|
|
253
|
+
"config.prompt or config.message",
|
|
254
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
255
|
+
),
|
|
256
|
+
{
|
|
257
|
+
action: "api",
|
|
258
|
+
status: "error",
|
|
259
|
+
runId: loaded.manifest.runId,
|
|
260
|
+
},
|
|
261
|
+
true,
|
|
262
|
+
);
|
|
263
|
+
if (operation === "follow-up-agent" && !prompt)
|
|
264
|
+
return result(
|
|
265
|
+
paramRequired(
|
|
266
|
+
"API follow-up-agent",
|
|
267
|
+
"config.prompt or config.message",
|
|
268
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
269
|
+
),
|
|
270
|
+
{
|
|
271
|
+
action: "api",
|
|
272
|
+
status: "error",
|
|
273
|
+
runId: loaded.manifest.runId,
|
|
274
|
+
},
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
try {
|
|
278
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, {
|
|
279
|
+
taskId: task.id,
|
|
280
|
+
agentId: agent.id,
|
|
281
|
+
operation:
|
|
282
|
+
operation === "resume-agent"
|
|
283
|
+
? "resume"
|
|
284
|
+
: operation === "follow-up-agent"
|
|
285
|
+
? "follow-up"
|
|
286
|
+
: operation === "steer-agent"
|
|
287
|
+
? "steer"
|
|
288
|
+
: "stop",
|
|
289
|
+
message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message,
|
|
290
|
+
});
|
|
291
|
+
const mailboxMessage =
|
|
292
|
+
operation === "steer-agent"
|
|
293
|
+
? appendSteeringMessage(loaded.manifest, {
|
|
294
|
+
taskId: task.id,
|
|
295
|
+
to: agent.id,
|
|
296
|
+
body: message ?? "Please report current status and wrap up if possible.",
|
|
297
|
+
status: "delivered",
|
|
298
|
+
data: {
|
|
299
|
+
source: "steer-agent",
|
|
300
|
+
liveControlRequestId: request.id,
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
: operation === "follow-up-agent" && prompt
|
|
304
|
+
? appendFollowUpMessage(loaded.manifest, {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
to: agent.id,
|
|
307
|
+
body: prompt,
|
|
308
|
+
status: "delivered",
|
|
309
|
+
data: {
|
|
310
|
+
source: "follow-up-agent",
|
|
311
|
+
liveControlRequestId: request.id,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
: undefined;
|
|
315
|
+
publishLiveControlRealtime(request);
|
|
316
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
317
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
318
|
+
type: "agent.control.queued",
|
|
319
|
+
runId: loaded.manifest.runId,
|
|
320
|
+
taskId: agent.taskId,
|
|
321
|
+
message: `Queued ${request.operation} control request for live agent.`,
|
|
322
|
+
data: {
|
|
323
|
+
request,
|
|
324
|
+
mailboxMessageId: mailboxMessage?.id,
|
|
325
|
+
realtime: true,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
|
|
329
|
+
action: "api",
|
|
330
|
+
status: "ok",
|
|
331
|
+
runId: loaded.manifest.runId,
|
|
332
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
333
|
+
});
|
|
334
|
+
} catch (queueError) {
|
|
335
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
336
|
+
return result(
|
|
337
|
+
message,
|
|
338
|
+
{
|
|
339
|
+
action: "api",
|
|
340
|
+
status: "error",
|
|
341
|
+
runId: loaded.manifest.runId,
|
|
342
|
+
},
|
|
343
|
+
true,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** Dispatcher map for the live-agent control group. Consumed by handleApi. */
|
|
350
|
+
export const AGENT_CONTROL_OPERATIONS: Record<string, ApiOperationHandler> = {
|
|
351
|
+
"nudge-agent": handleNudgeAgent,
|
|
352
|
+
"list-live-agents": handleListLiveAgents,
|
|
353
|
+
"steer-agent": handleLiveAgentControl,
|
|
354
|
+
"follow-up-agent": handleLiveAgentControl,
|
|
355
|
+
"stop-agent": handleLiveAgentControl,
|
|
356
|
+
"resume-agent": handleLiveAgentControl,
|
|
357
|
+
"interrupt-agent": handleLiveAgentControl,
|
|
358
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context for extracted `api` operation handlers (H3 phase 1+).
|
|
3
|
+
*
|
|
4
|
+
* The `handleApi` function in `src/extension/team-tool/api.ts` was 1222 lines
|
|
5
|
+
* with 35+ `if (operation === "…")` branches. This file defines the shared
|
|
6
|
+
* bundle of closure dependencies that each extracted handler receives, so
|
|
7
|
+
* sibling modules under `src/extension/team-tool/api/<group>.ts` can be
|
|
8
|
+
* pure-function-shaped without re-establishing the closure.
|
|
9
|
+
*
|
|
10
|
+
* Extraction pattern mirrors `merge-gate.ts` (improvement-plan-2026-08-09
|
|
11
|
+
* phase 5): the extracted module imports its own state/coordination helpers
|
|
12
|
+
* directly; only the request-scoped values (config, loaded run, params,
|
|
13
|
+
* extension context) are passed via this context.
|
|
14
|
+
*
|
|
15
|
+
* Phase 2 (2026-08-10): added `params` + `ctx` and made handlers async-capable
|
|
16
|
+
* (several operations await live-agent or runtime resolution).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TeamToolParamsValue } from "../../../schema/team-tool-schema.ts";
|
|
20
|
+
import type { TeamRunManifest, TeamTaskState } from "../../../state/types.ts";
|
|
21
|
+
import type { PiTeamsToolResult } from "../../tool-result.ts";
|
|
22
|
+
import type { result as resultFn, TeamContext } from "../context.ts";
|
|
23
|
+
import type { paramRequired as paramRequiredFn } from "../param-error.ts";
|
|
24
|
+
|
|
25
|
+
/** The config record produced by `configRecord(params.config)` in handleApi. */
|
|
26
|
+
export type ApiConfig = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
/** The loaded run snapshot returned by `loadRunManifestById`. */
|
|
29
|
+
export interface ApiLoadedRun {
|
|
30
|
+
manifest: TeamRunManifest;
|
|
31
|
+
tasks: TeamTaskState[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bundle of closure-scoped dependencies that handleApi establishes once and
|
|
36
|
+
* passes to each extracted handler. Adding a field here is cheaper than
|
|
37
|
+
* growing the closure of a 1200-line function.
|
|
38
|
+
*/
|
|
39
|
+
export interface ApiHandlerContext {
|
|
40
|
+
cfg: ApiConfig;
|
|
41
|
+
loaded: ApiLoadedRun;
|
|
42
|
+
result: typeof resultFn;
|
|
43
|
+
paramRequired: typeof paramRequiredFn;
|
|
44
|
+
/** The raw tool params (runId, force, etc.) the caller passed. */
|
|
45
|
+
params: TeamToolParamsValue;
|
|
46
|
+
/** The extension TeamContext (cwd, events, metricRegistry, config…). */
|
|
47
|
+
ctx: TeamContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Type alias for an extracted operation handler. Async-capable. */
|
|
51
|
+
export type ApiOperationHandler = (ctx: ApiHandlerContext) => PiTeamsToolResult | Promise<PiTeamsToolResult>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handler for operations that run BEFORE the runId guard (no loaded run).
|
|
55
|
+
* Currently: metrics-snapshot, inventory.
|
|
56
|
+
*/
|
|
57
|
+
export type ApiPreHandler = (ctx: Omit<ApiHandlerContext, "loaded">) => PiTeamsToolResult | Promise<PiTeamsToolResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted `api` operation handlers — heartbeat group (H3 phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
|
|
5
|
+
* byte-identical to the inline `if` blocks they replace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
|
|
9
|
+
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
10
|
+
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
11
|
+
import { saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
12
|
+
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
13
|
+
|
|
14
|
+
export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
15
|
+
const { cfg, loaded, result, paramRequired } = hctx;
|
|
16
|
+
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
17
|
+
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
18
|
+
if (!task)
|
|
19
|
+
return result(
|
|
20
|
+
paramRequired(
|
|
21
|
+
"API write-heartbeat",
|
|
22
|
+
"config.taskId matching a task id or step id",
|
|
23
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'write-heartbeat', taskId: '01_01-agent' } }",
|
|
24
|
+
),
|
|
25
|
+
{
|
|
26
|
+
action: "api",
|
|
27
|
+
status: "error",
|
|
28
|
+
runId: loaded.manifest.runId,
|
|
29
|
+
},
|
|
30
|
+
true,
|
|
31
|
+
);
|
|
32
|
+
try {
|
|
33
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
34
|
+
const heartbeat = touchWorkerHeartbeat(
|
|
35
|
+
task.heartbeat ?? {
|
|
36
|
+
workerId: task.id,
|
|
37
|
+
lastSeenAt: new Date().toISOString(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
const tasks = loaded.tasks.map((item) => (item.id === task.id ? { ...item, heartbeat } : item));
|
|
44
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
45
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
46
|
+
type: "worker.heartbeat",
|
|
47
|
+
runId: loaded.manifest.runId,
|
|
48
|
+
taskId: task.id,
|
|
49
|
+
data: { ...heartbeat },
|
|
50
|
+
});
|
|
51
|
+
return result(JSON.stringify(heartbeat, null, 2), {
|
|
52
|
+
action: "api",
|
|
53
|
+
status: "ok",
|
|
54
|
+
runId: loaded.manifest.runId,
|
|
55
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return result(
|
|
61
|
+
message,
|
|
62
|
+
{
|
|
63
|
+
action: "api",
|
|
64
|
+
status: "error",
|
|
65
|
+
runId: loaded.manifest.runId,
|
|
66
|
+
},
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Dispatcher map for the heartbeat group. Consumed by handleApi. */
|
|
73
|
+
export const HEARTBEAT_OPERATIONS: Record<string, ApiOperationHandler> = {
|
|
74
|
+
"write-heartbeat": handleWriteHeartbeat,
|
|
75
|
+
};
|