@alexgorbatchev/pi-agentation 3.0.0 → 3.1.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/README.md +48 -26
- package/agentation.ts +704 -80
- package/bin/pi-agentation +45 -2
- package/package.json +9 -7
- package/skills/agentation/SKILL.md +112 -0
- package/skills/agentation-fix-loop/SKILL.md +0 -199
package/agentation.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import process from "node:process";
|
|
3
6
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
7
|
+
const AGENTATION_SKILL_NAME = "agentation";
|
|
8
|
+
const AGENTATION_SKILL_PROMPT = `/skill:${AGENTATION_SKILL_NAME}`;
|
|
9
|
+
const AGENTATION_EXECUTABLE_ENV_NAME = "PI_AGENTATION_AGENTATION_BIN";
|
|
6
10
|
const PROJECT_SELECTION_ENTRY_TYPE = "agentation-project-selection";
|
|
11
|
+
const BATCH_CONTEXT_MESSAGE_TYPE = "agentation-batch-context";
|
|
7
12
|
const PROJECT_ID_PATTERN = /^projectId=(?:"([^"\r\n]+)"|'([^'\r\n]+)')$/;
|
|
13
|
+
const AGENTATION_SKILL_INVOCATION_PATTERN = new RegExp(`^${AGENTATION_SKILL_PROMPT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s+(.+))?$`);
|
|
14
|
+
const AGENTATION_ACTION_PATTERN = /\bagentation\s+(ack|resolve|reply|dismiss)\s+([^\s]+)/g;
|
|
15
|
+
const WATCH_TIMEOUT_SECONDS = "300";
|
|
16
|
+
const WATCH_BATCH_WINDOW = "10";
|
|
17
|
+
const LOOP_IDLE_WAIT_MS = 500;
|
|
18
|
+
const WATCH_RETRY_DELAY_MS = 5_000;
|
|
8
19
|
|
|
9
20
|
type ExecResult = Awaited<ReturnType<ExtensionAPI["exec"]>>;
|
|
21
|
+
type AgentationAction = "ack" | "resolve" | "reply" | "dismiss";
|
|
22
|
+
type BatchSource = "pending" | "watch";
|
|
10
23
|
|
|
11
24
|
type CommandOutcome = {
|
|
12
25
|
code: number | undefined;
|
|
@@ -20,9 +33,76 @@ interface IProjectSelectionData {
|
|
|
20
33
|
projectId: string;
|
|
21
34
|
}
|
|
22
35
|
|
|
36
|
+
interface IAgentationAnnotation extends Record<string, unknown> {
|
|
37
|
+
id: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface IAgentationBatchResponse {
|
|
41
|
+
annotations: IAgentationAnnotation[];
|
|
42
|
+
count: number;
|
|
43
|
+
timeout?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IAgentationWatchTimeoutResponse {
|
|
47
|
+
message?: string;
|
|
48
|
+
timeout: true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type AgentationPollResponse = IAgentationBatchResponse | IAgentationWatchTimeoutResponse;
|
|
52
|
+
|
|
53
|
+
interface IAnnotationProgress {
|
|
54
|
+
id: string;
|
|
55
|
+
isHandled: boolean;
|
|
56
|
+
lastAction?: AgentationAction;
|
|
57
|
+
wasAcknowledged: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface IActiveBatch {
|
|
61
|
+
annotations: IAgentationAnnotation[];
|
|
62
|
+
progressById: Map<string, IAnnotationProgress>;
|
|
63
|
+
projectId: string;
|
|
64
|
+
rawJson: string;
|
|
65
|
+
source: BatchSource;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type AgentationUiPhase = "error" | "initializing" | "processing" | "watching";
|
|
69
|
+
|
|
70
|
+
interface IAgentationUiState {
|
|
71
|
+
annotationCount?: number;
|
|
72
|
+
detail: string;
|
|
73
|
+
phase: AgentationUiPhase;
|
|
74
|
+
projectId: string | null;
|
|
75
|
+
source?: BatchSource;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ILoopRuntimeState {
|
|
79
|
+
activeBatch: IActiveBatch | null;
|
|
80
|
+
agentationExecutablePath: string | null;
|
|
81
|
+
currentContext: ExtensionContext | null;
|
|
82
|
+
currentProjectId: string | null;
|
|
83
|
+
hasNotifiedIncompleteBatch: boolean;
|
|
84
|
+
isLoopEnabled: boolean;
|
|
85
|
+
pendingLoopInvocationProjectId: string | null;
|
|
86
|
+
uiState: IAgentationUiState | null;
|
|
87
|
+
watchAbortController: AbortController | null;
|
|
88
|
+
watchGeneration: number;
|
|
89
|
+
watchTask: Promise<void> | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
23
92
|
export default function agentation(pi: ExtensionAPI): void {
|
|
24
|
-
|
|
25
|
-
|
|
93
|
+
const runtimeState: ILoopRuntimeState = {
|
|
94
|
+
activeBatch: null,
|
|
95
|
+
agentationExecutablePath: null,
|
|
96
|
+
currentContext: null,
|
|
97
|
+
currentProjectId: null,
|
|
98
|
+
hasNotifiedIncompleteBatch: false,
|
|
99
|
+
isLoopEnabled: true,
|
|
100
|
+
pendingLoopInvocationProjectId: null,
|
|
101
|
+
uiState: null,
|
|
102
|
+
watchAbortController: null,
|
|
103
|
+
watchGeneration: 0,
|
|
104
|
+
watchTask: null,
|
|
105
|
+
};
|
|
26
106
|
|
|
27
107
|
const isLoopSkillAvailable = (): boolean => {
|
|
28
108
|
return pi.getCommands().some((command) => {
|
|
@@ -30,18 +110,81 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
30
110
|
return false;
|
|
31
111
|
}
|
|
32
112
|
|
|
33
|
-
return command.name ===
|
|
113
|
+
return command.name === AGENTATION_SKILL_NAME || command.name === `skill:${AGENTATION_SKILL_NAME}`;
|
|
34
114
|
});
|
|
35
115
|
};
|
|
36
116
|
|
|
37
|
-
const
|
|
117
|
+
const setCurrentContext = (ctx: ExtensionContext): void => {
|
|
118
|
+
runtimeState.currentContext = ctx;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const clearUiState = (ctx: ExtensionContext | null): void => {
|
|
122
|
+
runtimeState.uiState = null;
|
|
123
|
+
if (ctx === null || !ctx.hasUI) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ctx.ui.setStatus("agentation", undefined);
|
|
128
|
+
ctx.ui.setWidget("agentation", undefined);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const setUiState = (ctx: ExtensionContext | null, uiState: IAgentationUiState): void => {
|
|
132
|
+
runtimeState.uiState = uiState;
|
|
133
|
+
if (ctx === null || !ctx.hasUI) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const statusText = formatWidgetTitle(uiState);
|
|
138
|
+
|
|
139
|
+
ctx.ui.setWidget("agentation", (_tui, theme) => {
|
|
140
|
+
const borderColorName = getUiPhaseColorName(uiState.phase);
|
|
141
|
+
const borderColor = (text: string): string => theme.fg(borderColorName, text);
|
|
142
|
+
const container = new Container();
|
|
143
|
+
const titleText = borderColor(theme.bold(statusText));
|
|
144
|
+
|
|
145
|
+
container.addChild(new DynamicBorder(borderColor));
|
|
146
|
+
container.addChild(new Text(titleText, 1, 0));
|
|
147
|
+
container.addChild(new DynamicBorder(borderColor));
|
|
148
|
+
return container;
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const reportError = (ctx: ExtensionContext | null, message: string): void => {
|
|
38
153
|
console.error(message);
|
|
39
|
-
ctx
|
|
154
|
+
setUiState(ctx, {
|
|
155
|
+
detail: message,
|
|
156
|
+
phase: "error",
|
|
157
|
+
projectId: runtimeState.currentProjectId,
|
|
158
|
+
source: runtimeState.activeBatch?.source,
|
|
159
|
+
});
|
|
160
|
+
ctx?.ui.notify(message, "error");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const stopWatchLoop = (): void => {
|
|
164
|
+
runtimeState.watchGeneration += 1;
|
|
165
|
+
runtimeState.watchAbortController?.abort();
|
|
166
|
+
runtimeState.watchAbortController = null;
|
|
167
|
+
runtimeState.watchTask = null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const resetRuntimeStateForSession = (ctx: ExtensionContext): void => {
|
|
171
|
+
clearUiState(runtimeState.currentContext);
|
|
172
|
+
stopWatchLoop();
|
|
173
|
+
setCurrentContext(ctx);
|
|
174
|
+
runtimeState.activeBatch = null;
|
|
175
|
+
runtimeState.agentationExecutablePath = resolveAgentationExecutablePath(ctx.cwd);
|
|
176
|
+
runtimeState.currentProjectId = restoreProjectSelection(ctx);
|
|
177
|
+
runtimeState.hasNotifiedIncompleteBatch = false;
|
|
178
|
+
runtimeState.isLoopEnabled = true;
|
|
179
|
+
runtimeState.pendingLoopInvocationProjectId = null;
|
|
40
180
|
};
|
|
41
181
|
|
|
42
182
|
const shutdownForFatalError = (ctx: ExtensionContext, message: string): void => {
|
|
43
|
-
|
|
44
|
-
|
|
183
|
+
stopWatchLoop();
|
|
184
|
+
runtimeState.activeBatch = null;
|
|
185
|
+
runtimeState.currentProjectId = null;
|
|
186
|
+
runtimeState.isLoopEnabled = false;
|
|
187
|
+
runtimeState.pendingLoopInvocationProjectId = null;
|
|
45
188
|
process.exitCode = 1;
|
|
46
189
|
reportError(ctx, message);
|
|
47
190
|
ctx.shutdown();
|
|
@@ -52,30 +195,16 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
52
195
|
return true;
|
|
53
196
|
}
|
|
54
197
|
|
|
55
|
-
shutdownForFatalError(ctx, `Missing required skill ${
|
|
198
|
+
shutdownForFatalError(ctx, `Missing required skill ${AGENTATION_SKILL_PROMPT}. Exiting.`);
|
|
56
199
|
return false;
|
|
57
200
|
};
|
|
58
201
|
|
|
59
|
-
const queueLoopPrompt = (projectId: string, deliverAsFollowUp: boolean): void => {
|
|
60
|
-
if (!isLoopEnabled) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const loopPrompt = `${LOOP_PROMPT} ${projectId}`;
|
|
65
|
-
if (deliverAsFollowUp) {
|
|
66
|
-
pi.sendUserMessage(loopPrompt, { deliverAs: "followUp" });
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
pi.sendUserMessage(loopPrompt);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
202
|
const persistProjectSelection = (projectId: string): void => {
|
|
74
|
-
if (currentProjectId === projectId) {
|
|
203
|
+
if (runtimeState.currentProjectId === projectId) {
|
|
75
204
|
return;
|
|
76
205
|
}
|
|
77
206
|
|
|
78
|
-
currentProjectId = projectId;
|
|
207
|
+
runtimeState.currentProjectId = projectId;
|
|
79
208
|
pi.appendEntry(PROJECT_SELECTION_ENTRY_TYPE, { projectId });
|
|
80
209
|
};
|
|
81
210
|
|
|
@@ -103,13 +232,12 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
103
232
|
}
|
|
104
233
|
}
|
|
105
234
|
|
|
106
|
-
currentProjectId = latestProjectId;
|
|
107
235
|
return latestProjectId;
|
|
108
236
|
};
|
|
109
237
|
|
|
110
|
-
const execCommand = async (command: string, args: string[]): Promise<CommandOutcome> => {
|
|
238
|
+
const execCommand = async (command: string, args: string[], signal?: AbortSignal): Promise<CommandOutcome> => {
|
|
111
239
|
try {
|
|
112
|
-
const result: ExecResult = await pi.exec(command, args);
|
|
240
|
+
const result: ExecResult = await pi.exec(command, args, { signal });
|
|
113
241
|
return {
|
|
114
242
|
code: result.code,
|
|
115
243
|
killed: result.killed,
|
|
@@ -127,11 +255,16 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
127
255
|
}
|
|
128
256
|
};
|
|
129
257
|
|
|
258
|
+
const execAgentationCommand = async (args: string[], signal?: AbortSignal): Promise<CommandOutcome> => {
|
|
259
|
+
const agentationExecutablePath = runtimeState.agentationExecutablePath ?? "agentation";
|
|
260
|
+
return execCommand(agentationExecutablePath, args, signal);
|
|
261
|
+
};
|
|
262
|
+
|
|
130
263
|
const listKnownProjectIds = async (): Promise<{ errorMessage?: string; projectIds: string[] }> => {
|
|
131
|
-
let projectsResult = await
|
|
264
|
+
let projectsResult = await execAgentationCommand(["projects", "--json"]);
|
|
132
265
|
if (!didCommandSucceed(projectsResult)) {
|
|
133
|
-
await
|
|
134
|
-
projectsResult = await
|
|
266
|
+
await execAgentationCommand(["start", "--background"]);
|
|
267
|
+
projectsResult = await execAgentationCommand(["projects", "--json"]);
|
|
135
268
|
}
|
|
136
269
|
|
|
137
270
|
if (!didCommandSucceed(projectsResult)) {
|
|
@@ -233,18 +366,253 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
233
366
|
return selectedProjectId;
|
|
234
367
|
};
|
|
235
368
|
|
|
236
|
-
const
|
|
237
|
-
|
|
369
|
+
const clearActiveBatch = (): void => {
|
|
370
|
+
runtimeState.activeBatch = null;
|
|
371
|
+
runtimeState.hasNotifiedIncompleteBatch = false;
|
|
372
|
+
};
|
|
238
373
|
|
|
239
|
-
|
|
374
|
+
const createActiveBatch = (
|
|
375
|
+
projectId: string,
|
|
376
|
+
source: BatchSource,
|
|
377
|
+
batchResponse: IAgentationBatchResponse
|
|
378
|
+
): IActiveBatch => {
|
|
379
|
+
const progressById = new Map<string, IAnnotationProgress>();
|
|
380
|
+
for (const annotation of batchResponse.annotations) {
|
|
381
|
+
progressById.set(annotation.id, {
|
|
382
|
+
id: annotation.id,
|
|
383
|
+
isHandled: false,
|
|
384
|
+
wasAcknowledged: false,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
annotations: batchResponse.annotations,
|
|
390
|
+
progressById,
|
|
391
|
+
projectId,
|
|
392
|
+
rawJson: JSON.stringify(batchResponse, null, 2),
|
|
393
|
+
source,
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const createBatchContextMessage = (activeBatch: IActiveBatch): string => {
|
|
398
|
+
return [
|
|
399
|
+
"Agentation extension batch context:",
|
|
400
|
+
`- projectId: ${activeBatch.projectId}`,
|
|
401
|
+
`- source: ${activeBatch.source}`,
|
|
402
|
+
`- annotationCount: ${activeBatch.annotations.length}`,
|
|
403
|
+
"- The extension already fetched this batch.",
|
|
404
|
+
"- Do NOT call `agentation pending`, `agentation watch`, `agentation start`, `agentation status`, or `agentation projects`.",
|
|
405
|
+
"- Use `agentation ack`, `agentation resolve`, `agentation reply`, and `agentation dismiss` only for annotation IDs from this batch.",
|
|
406
|
+
"- Run those Agentation action commands as separate bash commands so the extension can track batch completion correctly.",
|
|
407
|
+
"",
|
|
408
|
+
"Current batch JSON:",
|
|
409
|
+
"```json",
|
|
410
|
+
activeBatch.rawJson,
|
|
411
|
+
"```",
|
|
412
|
+
].join("\n");
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const createNoBatchContextMessage = (projectId: string): string => {
|
|
416
|
+
return [
|
|
417
|
+
"Agentation extension batch context:",
|
|
418
|
+
`- projectId: ${projectId}`,
|
|
419
|
+
"- There is no active batch available right now.",
|
|
420
|
+
"- Do NOT call `agentation pending` or `agentation watch` yourself; the extension owns polling.",
|
|
421
|
+
"- If you need to recover from an interrupted batch, restart pi-agentation.",
|
|
422
|
+
].join("\n");
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const dispatchActiveBatch = (projectId: string): void => {
|
|
426
|
+
const loopPrompt = `${AGENTATION_SKILL_PROMPT} ${projectId}`;
|
|
427
|
+
const currentContext = runtimeState.currentContext;
|
|
428
|
+
const activeBatch = runtimeState.activeBatch;
|
|
429
|
+
const source = activeBatch?.source;
|
|
430
|
+
|
|
431
|
+
runtimeState.pendingLoopInvocationProjectId = projectId;
|
|
432
|
+
runtimeState.hasNotifiedIncompleteBatch = false;
|
|
433
|
+
setUiState(currentContext, {
|
|
434
|
+
annotationCount: activeBatch?.annotations.length,
|
|
435
|
+
detail:
|
|
436
|
+
currentContext !== null && !currentContext.isIdle()
|
|
437
|
+
? `Triggered ${AGENTATION_SKILL_PROMPT}. Batch queued; pi is busy.`
|
|
438
|
+
: `Triggered ${AGENTATION_SKILL_PROMPT}. Pi is processing the batch.`,
|
|
439
|
+
phase: "processing",
|
|
440
|
+
projectId,
|
|
441
|
+
source,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (currentContext !== null && !currentContext.isIdle()) {
|
|
445
|
+
pi.sendUserMessage(loopPrompt, { deliverAs: "followUp" });
|
|
240
446
|
return;
|
|
241
447
|
}
|
|
242
448
|
|
|
449
|
+
pi.sendUserMessage(loopPrompt);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const dispatchBatch = (projectId: string, source: BatchSource, batchResponse: IAgentationBatchResponse): void => {
|
|
453
|
+
runtimeState.activeBatch = createActiveBatch(projectId, source, batchResponse);
|
|
454
|
+
setUiState(runtimeState.currentContext, {
|
|
455
|
+
annotationCount: batchResponse.annotations.length,
|
|
456
|
+
detail: `${batchResponse.annotations.length} annotation${batchResponse.annotations.length === 1 ? "" : "s"} received. Triggering ${AGENTATION_SKILL_PROMPT}.`,
|
|
457
|
+
phase: "processing",
|
|
458
|
+
projectId,
|
|
459
|
+
source,
|
|
460
|
+
});
|
|
461
|
+
runtimeState.currentContext?.ui.notify(
|
|
462
|
+
`Agentation batch ready for ${projectId} (${batchResponse.annotations.length} annotation${batchResponse.annotations.length === 1 ? "" : "s"
|
|
463
|
+
})`,
|
|
464
|
+
"info"
|
|
465
|
+
);
|
|
466
|
+
dispatchActiveBatch(projectId);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const shouldContinueWatchLoop = (generation: number, projectId: string): boolean => {
|
|
470
|
+
return (
|
|
471
|
+
runtimeState.isLoopEnabled &&
|
|
472
|
+
runtimeState.currentProjectId === projectId &&
|
|
473
|
+
runtimeState.watchGeneration === generation
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const runWatchLoop = async (generation: number, projectId: string, signal: AbortSignal): Promise<void> => {
|
|
478
|
+
let nextBatchSource: BatchSource = "pending";
|
|
479
|
+
|
|
480
|
+
while (shouldContinueWatchLoop(generation, projectId)) {
|
|
481
|
+
if (signal.aborted) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const currentContext = runtimeState.currentContext;
|
|
486
|
+
if (runtimeState.activeBatch !== null || (currentContext !== null && !currentContext.isIdle())) {
|
|
487
|
+
await waitForDelay(LOOP_IDLE_WAIT_MS, signal);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const commandArgs =
|
|
492
|
+
nextBatchSource === "pending"
|
|
493
|
+
? ["pending", projectId, "--json"]
|
|
494
|
+
: ["watch", projectId, "--timeout", WATCH_TIMEOUT_SECONDS, "--batch-window", WATCH_BATCH_WINDOW, "--json"];
|
|
495
|
+
const commandOutcome = await execAgentationCommand(commandArgs, signal);
|
|
496
|
+
if (!shouldContinueWatchLoop(generation, projectId) || signal.aborted) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!didCommandSucceed(commandOutcome)) {
|
|
501
|
+
if (commandOutcome.killed && signal.aborted) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
reportError(
|
|
506
|
+
runtimeState.currentContext,
|
|
507
|
+
`Agentation ${nextBatchSource} failed for ${projectId}: ${formatCommandOutcome(commandOutcome)}`
|
|
508
|
+
);
|
|
509
|
+
await waitForDelay(WATCH_RETRY_DELAY_MS, signal);
|
|
510
|
+
nextBatchSource = "pending";
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const pollResponse = parseAgentationPollResponse(commandOutcome.stdout);
|
|
515
|
+
if (pollResponse === null) {
|
|
516
|
+
reportError(
|
|
517
|
+
runtimeState.currentContext,
|
|
518
|
+
`Agentation ${nextBatchSource} returned an unexpected JSON response for ${projectId}.`
|
|
519
|
+
);
|
|
520
|
+
await waitForDelay(WATCH_RETRY_DELAY_MS, signal);
|
|
521
|
+
nextBatchSource = "pending";
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (isAgentationWatchTimeoutResponse(pollResponse)) {
|
|
526
|
+
const detail = pollResponse.message ?? `No new annotations in the last ${WATCH_TIMEOUT_SECONDS}s. Restarting live watch.`;
|
|
527
|
+
setUiState(runtimeState.currentContext, {
|
|
528
|
+
detail,
|
|
529
|
+
phase: "watching",
|
|
530
|
+
projectId,
|
|
531
|
+
source: nextBatchSource,
|
|
532
|
+
});
|
|
533
|
+
nextBatchSource = "watch";
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (pollResponse.annotations.length === 0) {
|
|
538
|
+
const detail =
|
|
539
|
+
nextBatchSource === "pending"
|
|
540
|
+
? "No pending annotations. Live watch active."
|
|
541
|
+
: pollResponse.timeout === true
|
|
542
|
+
? `No new annotations in the last ${WATCH_TIMEOUT_SECONDS}s. Restarting live watch.`
|
|
543
|
+
: "Live watch active.";
|
|
544
|
+
setUiState(runtimeState.currentContext, {
|
|
545
|
+
detail,
|
|
546
|
+
phase: "watching",
|
|
547
|
+
projectId,
|
|
548
|
+
source: nextBatchSource,
|
|
549
|
+
});
|
|
550
|
+
nextBatchSource = "watch";
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
dispatchBatch(projectId, nextBatchSource, pollResponse);
|
|
555
|
+
nextBatchSource = "watch";
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const startWatchLoop = (ctx: ExtensionContext, projectId: string): void => {
|
|
560
|
+
setCurrentContext(ctx);
|
|
561
|
+
|
|
562
|
+
if (!runtimeState.isLoopEnabled) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!ensureLoopSkillAvailable(ctx)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (runtimeState.watchTask !== null && runtimeState.currentProjectId === projectId) {
|
|
571
|
+
setUiState(ctx, {
|
|
572
|
+
detail: "Live watch active.",
|
|
573
|
+
phase: "watching",
|
|
574
|
+
projectId,
|
|
575
|
+
source: "watch",
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
stopWatchLoop();
|
|
581
|
+
runtimeState.currentProjectId = projectId;
|
|
582
|
+
setUiState(ctx, {
|
|
583
|
+
detail: "Checking queue…",
|
|
584
|
+
phase: "initializing",
|
|
585
|
+
projectId,
|
|
586
|
+
source: "pending",
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const watchAbortController = new AbortController();
|
|
590
|
+
const generation = runtimeState.watchGeneration;
|
|
591
|
+
runtimeState.watchAbortController = watchAbortController;
|
|
592
|
+
runtimeState.watchTask = runWatchLoop(generation, projectId, watchAbortController.signal).finally(() => {
|
|
593
|
+
if (runtimeState.watchGeneration !== generation) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
runtimeState.watchAbortController = null;
|
|
598
|
+
runtimeState.watchTask = null;
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const initializeLoopForSession = async (ctx: ExtensionContext): Promise<void> => {
|
|
603
|
+
resetRuntimeStateForSession(ctx);
|
|
604
|
+
setUiState(ctx, {
|
|
605
|
+
detail: "Resolving project…",
|
|
606
|
+
phase: "initializing",
|
|
607
|
+
projectId: runtimeState.currentProjectId,
|
|
608
|
+
source: "pending",
|
|
609
|
+
});
|
|
610
|
+
|
|
243
611
|
if (!ensureLoopSkillAvailable(ctx)) {
|
|
244
612
|
return;
|
|
245
613
|
}
|
|
246
614
|
|
|
247
|
-
let projectId = currentProjectId;
|
|
615
|
+
let projectId = runtimeState.currentProjectId;
|
|
248
616
|
if (projectId === null) {
|
|
249
617
|
projectId = await resolveProjectId(ctx);
|
|
250
618
|
if (projectId === null) {
|
|
@@ -253,8 +621,51 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
253
621
|
persistProjectSelection(projectId);
|
|
254
622
|
}
|
|
255
623
|
|
|
256
|
-
ctx
|
|
257
|
-
|
|
624
|
+
startWatchLoop(ctx, projectId);
|
|
625
|
+
ctx.ui.notify(`Agentation extension watch started for ${projectId}`, "info");
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const updateBatchProgressFromCommand = (command: string): void => {
|
|
629
|
+
const activeBatch = runtimeState.activeBatch;
|
|
630
|
+
if (activeBatch === null) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const actions = extractAgentationActionsFromCommand(command);
|
|
635
|
+
if (actions.length === 0) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
for (const action of actions) {
|
|
640
|
+
const annotationProgress = activeBatch.progressById.get(action.annotationId);
|
|
641
|
+
if (annotationProgress === undefined) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (action.action === "ack") {
|
|
646
|
+
annotationProgress.wasAcknowledged = true;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
annotationProgress.isHandled = true;
|
|
651
|
+
annotationProgress.lastAction = action.action;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const isBatchHandled = Array.from(activeBatch.progressById.values()).every((annotationProgress) => {
|
|
655
|
+
return annotationProgress.isHandled;
|
|
656
|
+
});
|
|
657
|
+
if (!isBatchHandled) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
clearActiveBatch();
|
|
662
|
+
setUiState(runtimeState.currentContext, {
|
|
663
|
+
detail: "Batch completed. Live watch active.",
|
|
664
|
+
phase: "watching",
|
|
665
|
+
projectId: activeBatch.projectId,
|
|
666
|
+
source: "watch",
|
|
667
|
+
});
|
|
668
|
+
runtimeState.currentContext?.ui.notify(`Agentation batch completed for ${activeBatch.projectId}`, "success");
|
|
258
669
|
};
|
|
259
670
|
|
|
260
671
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -269,62 +680,108 @@ export default function agentation(pi: ExtensionAPI): void {
|
|
|
269
680
|
await initializeLoopForSession(ctx);
|
|
270
681
|
});
|
|
271
682
|
|
|
272
|
-
pi.on("
|
|
273
|
-
|
|
274
|
-
|
|
683
|
+
pi.on("input", async (event, ctx) => {
|
|
684
|
+
setCurrentContext(ctx);
|
|
685
|
+
runtimeState.pendingLoopInvocationProjectId = parseLoopInvocationProjectId(event.text);
|
|
686
|
+
return { action: "continue" };
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
690
|
+
setCurrentContext(ctx);
|
|
691
|
+
|
|
692
|
+
const pendingProjectId = runtimeState.pendingLoopInvocationProjectId;
|
|
693
|
+
if (pendingProjectId === null) {
|
|
694
|
+
return undefined;
|
|
275
695
|
}
|
|
276
696
|
|
|
277
|
-
|
|
278
|
-
|
|
697
|
+
runtimeState.pendingLoopInvocationProjectId = null;
|
|
698
|
+
|
|
699
|
+
const activeBatch = runtimeState.activeBatch;
|
|
700
|
+
const content =
|
|
701
|
+
activeBatch !== null && activeBatch.projectId === pendingProjectId
|
|
702
|
+
? createBatchContextMessage(activeBatch)
|
|
703
|
+
: createNoBatchContextMessage(pendingProjectId);
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
message: {
|
|
707
|
+
content,
|
|
708
|
+
customType: BATCH_CONTEXT_MESSAGE_TYPE,
|
|
709
|
+
display: false,
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
715
|
+
setCurrentContext(ctx);
|
|
716
|
+
|
|
717
|
+
if (event.toolName !== "bash" || event.isError || !isBashToolInput(event.input)) {
|
|
718
|
+
return undefined;
|
|
279
719
|
}
|
|
280
720
|
|
|
281
|
-
|
|
282
|
-
|
|
721
|
+
updateBatchProgressFromCommand(event.input.command);
|
|
722
|
+
return undefined;
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
726
|
+
setCurrentContext(ctx);
|
|
727
|
+
|
|
728
|
+
if (runtimeState.activeBatch === null || runtimeState.hasNotifiedIncompleteBatch) {
|
|
283
729
|
return;
|
|
284
730
|
}
|
|
285
731
|
|
|
286
|
-
|
|
732
|
+
runtimeState.hasNotifiedIncompleteBatch = true;
|
|
733
|
+
setUiState(ctx, {
|
|
734
|
+
annotationCount: runtimeState.activeBatch.annotations.length,
|
|
735
|
+
detail: "Batch still incomplete. Restart pi-agentation to retry.",
|
|
736
|
+
phase: "processing",
|
|
737
|
+
projectId: runtimeState.activeBatch.projectId,
|
|
738
|
+
source: runtimeState.activeBatch.source,
|
|
739
|
+
});
|
|
740
|
+
ctx.ui.notify("Current Agentation batch is still incomplete. Restart pi-agentation to retry.", "warning");
|
|
287
741
|
});
|
|
288
742
|
|
|
289
743
|
pi.on("session_shutdown", async () => {
|
|
290
|
-
|
|
291
|
-
|
|
744
|
+
clearUiState(runtimeState.currentContext);
|
|
745
|
+
stopWatchLoop();
|
|
746
|
+
runtimeState.activeBatch = null;
|
|
747
|
+
runtimeState.agentationExecutablePath = null;
|
|
748
|
+
runtimeState.currentContext = null;
|
|
749
|
+
runtimeState.currentProjectId = null;
|
|
750
|
+
runtimeState.pendingLoopInvocationProjectId = null;
|
|
292
751
|
});
|
|
293
752
|
|
|
294
|
-
|
|
295
|
-
description: "Start the automatic Agentation fix loop",
|
|
296
|
-
handler: async (_args, ctx) => {
|
|
297
|
-
if (isLoopEnabled) {
|
|
298
|
-
ctx.ui.notify("Agentation loop is already running", "info");
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (!ensureLoopSkillAvailable(ctx)) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
753
|
+
}
|
|
305
754
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
persistProjectSelection(projectId);
|
|
313
|
-
}
|
|
755
|
+
function formatWidgetTitle(uiState: IAgentationUiState): string {
|
|
756
|
+
const projectLabel = uiState.projectId ?? "resolving";
|
|
757
|
+
const phaseLabel = formatUiPhase(uiState.phase);
|
|
758
|
+
return `Agentation Fork / ${projectLabel} / ${phaseLabel}`;
|
|
759
|
+
}
|
|
314
760
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
761
|
+
function getUiPhaseColorName(phase: AgentationUiPhase): "accent" | "error" | "success" | "warning" {
|
|
762
|
+
switch (phase) {
|
|
763
|
+
case "error":
|
|
764
|
+
return "error";
|
|
765
|
+
case "initializing":
|
|
766
|
+
return "warning";
|
|
767
|
+
case "processing":
|
|
768
|
+
return "accent";
|
|
769
|
+
case "watching":
|
|
770
|
+
return "success";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
320
773
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
774
|
+
function formatUiPhase(phase: AgentationUiPhase): string {
|
|
775
|
+
switch (phase) {
|
|
776
|
+
case "error":
|
|
777
|
+
return "Error";
|
|
778
|
+
case "initializing":
|
|
779
|
+
return "Initializing";
|
|
780
|
+
case "processing":
|
|
781
|
+
return "Running";
|
|
782
|
+
case "watching":
|
|
783
|
+
return "Watching";
|
|
784
|
+
}
|
|
328
785
|
}
|
|
329
786
|
|
|
330
787
|
function didCommandSucceed(commandOutcome: CommandOutcome): boolean {
|
|
@@ -405,6 +862,110 @@ function extractProjectIdsFromRgOutput(output: string): string[] {
|
|
|
405
862
|
return normalizeProjectIds(projectIds);
|
|
406
863
|
}
|
|
407
864
|
|
|
865
|
+
function resolveAgentationExecutablePath(cwd: string): string {
|
|
866
|
+
const explicitExecutablePath = process.env[AGENTATION_EXECUTABLE_ENV_NAME];
|
|
867
|
+
if (isExecutablePath(explicitExecutablePath)) {
|
|
868
|
+
return explicitExecutablePath;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const localExecutablePath = findNearestNodeModulesExecutable("agentation", cwd);
|
|
872
|
+
if (localExecutablePath !== null) {
|
|
873
|
+
return localExecutablePath;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return "agentation";
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function findNearestNodeModulesExecutable(executableName: string, startDirectory: string): string | null {
|
|
880
|
+
let currentDirectory = path.resolve(startDirectory);
|
|
881
|
+
|
|
882
|
+
while (true) {
|
|
883
|
+
const candidateExecutablePath = path.join(currentDirectory, "node_modules", ".bin", executableName);
|
|
884
|
+
if (isExecutablePath(candidateExecutablePath)) {
|
|
885
|
+
return candidateExecutablePath;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
889
|
+
if (parentDirectory === currentDirectory) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
currentDirectory = parentDirectory;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function isExecutablePath(filePath: string | undefined): filePath is string {
|
|
898
|
+
if (typeof filePath !== "string" || filePath.trim() === "") {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
904
|
+
return true;
|
|
905
|
+
} catch {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function parseLoopInvocationProjectId(text: string): string | null {
|
|
911
|
+
const firstLine = text.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
912
|
+
const match = AGENTATION_SKILL_INVOCATION_PATTERN.exec(firstLine);
|
|
913
|
+
const projectId = match?.[1];
|
|
914
|
+
if (projectId === undefined) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return normalizeProjectId(projectId);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function extractAgentationActionsFromCommand(
|
|
922
|
+
command: string
|
|
923
|
+
): Array<{ action: AgentationAction; annotationId: string }> {
|
|
924
|
+
const actions: Array<{ action: AgentationAction; annotationId: string }> = [];
|
|
925
|
+
|
|
926
|
+
for (const match of command.matchAll(AGENTATION_ACTION_PATTERN)) {
|
|
927
|
+
const action = match[1];
|
|
928
|
+
const annotationId = match[2];
|
|
929
|
+
if (action === undefined || annotationId === undefined || !isAgentationAction(action)) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
actions.push({ action, annotationId });
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return actions;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function waitForDelay(milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
940
|
+
if (signal.aborted) {
|
|
941
|
+
return Promise.resolve();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return new Promise((resolve) => {
|
|
945
|
+
const timeoutHandle = setTimeout(() => {
|
|
946
|
+
signal.removeEventListener("abort", onAbort);
|
|
947
|
+
resolve();
|
|
948
|
+
}, milliseconds);
|
|
949
|
+
|
|
950
|
+
const onAbort = (): void => {
|
|
951
|
+
clearTimeout(timeoutHandle);
|
|
952
|
+
signal.removeEventListener("abort", onAbort);
|
|
953
|
+
resolve();
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function parseAgentationPollResponse(jsonText: string): AgentationPollResponse | null {
|
|
961
|
+
try {
|
|
962
|
+
const parsed = JSON.parse(jsonText);
|
|
963
|
+
return isAgentationPollResponse(parsed) ? parsed : null;
|
|
964
|
+
} catch {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
408
969
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
409
970
|
return typeof value === "object" && value !== null;
|
|
410
971
|
}
|
|
@@ -422,6 +983,69 @@ function isProjectSelectionData(value: unknown): value is IProjectSelectionData
|
|
|
422
983
|
return typeof projectId === "string" && projectId.trim() !== "";
|
|
423
984
|
}
|
|
424
985
|
|
|
986
|
+
function isAgentationAnnotation(value: unknown): value is IAgentationAnnotation {
|
|
987
|
+
if (!isRecord(value)) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return typeof value["id"] === "string" && value["id"].trim() !== "";
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isAgentationPollResponse(value: unknown): value is AgentationPollResponse {
|
|
995
|
+
return isAgentationBatchResponse(value) || isAgentationWatchTimeoutResponse(value);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function isAgentationBatchResponse(value: unknown): value is IAgentationBatchResponse {
|
|
999
|
+
if (!isRecord(value)) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const count = value["count"];
|
|
1004
|
+
const annotations = value["annotations"];
|
|
1005
|
+
const timeout = value["timeout"];
|
|
1006
|
+
|
|
1007
|
+
if (typeof count !== "number" || !Array.isArray(annotations) || !annotations.every(isAgentationAnnotation)) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (timeout !== undefined && typeof timeout !== "boolean") {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function isAgentationWatchTimeoutResponse(value: unknown): value is IAgentationWatchTimeoutResponse {
|
|
1019
|
+
if (!isRecord(value)) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const timeout = value["timeout"];
|
|
1024
|
+
const message = value["message"];
|
|
1025
|
+
|
|
1026
|
+
if (timeout !== true) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (message !== undefined && typeof message !== "string") {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function isBashToolInput(value: unknown): value is { command: string } {
|
|
1038
|
+
if (!isRecord(value)) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return typeof value["command"] === "string";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function isAgentationAction(value: string): value is AgentationAction {
|
|
1046
|
+
return value === "ack" || value === "resolve" || value === "reply" || value === "dismiss";
|
|
1047
|
+
}
|
|
1048
|
+
|
|
425
1049
|
function parseJsonStringArray(jsonText: string): string[] | null {
|
|
426
1050
|
try {
|
|
427
1051
|
const parsed = JSON.parse(jsonText);
|