macro-agent 0.0.17 → 0.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/dist/acp/macro-agent.d.ts +2 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +52 -20
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +6 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +103 -37
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +1 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +57 -8
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/store/event-store.d.ts +5 -0
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +34 -0
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +5 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/acp/__tests__/history.test.ts +8 -4
- package/src/acp/macro-agent.ts +60 -26
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +802 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +461 -2
- package/src/map/adapter/acp-over-map.ts +116 -43
- package/src/map/adapter/map-adapter.ts +79 -9
- package/src/store/__tests__/event-store.test.ts +40 -0
- package/src/store/event-store.ts +42 -0
- package/src/store/types/agents.ts +1 -0
|
@@ -15,6 +15,22 @@ import type { AgentId } from "../../store/types/index.js";
|
|
|
15
15
|
import { SessionMapper } from "../../acp/session-mapper.js";
|
|
16
16
|
import type { ACPSessionId } from "../../acp/types.js";
|
|
17
17
|
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────
|
|
19
|
+
// Helpers
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Extract a plain-text output string from `rawOutput` (string | ContentBlock[] | undefined). */
|
|
23
|
+
function extractToolOutput(rawOutput: unknown): string | undefined {
|
|
24
|
+
if (typeof rawOutput === "string") return rawOutput;
|
|
25
|
+
if (Array.isArray(rawOutput)) {
|
|
26
|
+
return rawOutput
|
|
27
|
+
.filter((item: any) => item.type === "text" && typeof item.text === "string")
|
|
28
|
+
.map((item: any) => item.text as string)
|
|
29
|
+
.join("\n") || undefined;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
// ─────────────────────────────────────────────────────────────────
|
|
19
35
|
// Types
|
|
20
36
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -79,6 +95,20 @@ export class ACPOverMAPHandler {
|
|
|
79
95
|
this.defaultCwd = config.defaultCwd ?? process.cwd();
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Abort all active streams targeting a specific agent.
|
|
100
|
+
* Called when an agent is stopped via MAP protocol to interrupt
|
|
101
|
+
* any in-progress ACP prompt streaming.
|
|
102
|
+
*/
|
|
103
|
+
abortStreamsForAgent(agentId: AgentId): void {
|
|
104
|
+
for (const [streamId, streamState] of this.streams) {
|
|
105
|
+
if (streamState.agentId === agentId) {
|
|
106
|
+
console.error(`[ACP-over-MAP] Aborting stream ${streamId} for stopped agent ${agentId}`);
|
|
107
|
+
streamState.abortController.abort();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
/**
|
|
83
113
|
* Process an ACP request and return the response.
|
|
84
114
|
* @param targetAgentId - Target agent for the request
|
|
@@ -386,11 +416,17 @@ export class ACPOverMAPHandler {
|
|
|
386
416
|
this.ensureConversation(sessionId as ACPSessionId, agentId);
|
|
387
417
|
|
|
388
418
|
// Accumulate response content for history recording
|
|
389
|
-
const buffer: {
|
|
390
|
-
|
|
391
|
-
toolCalls: [],
|
|
419
|
+
const buffer: { parts: Array<{ type: "text"; text: string } | ({ type: "tool" } & Record<string, unknown>)> } = {
|
|
420
|
+
parts: [],
|
|
392
421
|
};
|
|
393
422
|
|
|
423
|
+
// Track latest plan for persistence
|
|
424
|
+
let latestPlan: Array<{ content: string; priority: string; status: string }> | null = null;
|
|
425
|
+
|
|
426
|
+
// Track tool info from initial tool_call events (title, name, input)
|
|
427
|
+
// so we can merge them when tool_call_update arrives with status "completed"
|
|
428
|
+
const toolInfoCache = new Map<string, { title?: string; name?: string; input?: unknown }>();
|
|
429
|
+
|
|
394
430
|
try {
|
|
395
431
|
// Stream responses from the agent
|
|
396
432
|
let updateCount = 0;
|
|
@@ -400,23 +436,49 @@ export class ACPOverMAPHandler {
|
|
|
400
436
|
return { stopReason: "cancelled" };
|
|
401
437
|
}
|
|
402
438
|
|
|
403
|
-
// Accumulate content for history persistence
|
|
439
|
+
// Accumulate content for history persistence (preserving text/tool interleaving order)
|
|
404
440
|
const u = update as Record<string, unknown>;
|
|
405
441
|
const updateType = u.sessionUpdate as string ?? u.type as string;
|
|
406
442
|
if (updateType === "agent_message_chunk") {
|
|
407
443
|
const content = u.content as { type?: string; text?: string } | undefined;
|
|
408
444
|
if (content?.text) {
|
|
409
|
-
buffer.
|
|
445
|
+
const last = buffer.parts[buffer.parts.length - 1];
|
|
446
|
+
if (last && last.type === "text") {
|
|
447
|
+
last.text += content.text;
|
|
448
|
+
} else {
|
|
449
|
+
buffer.parts.push({ type: "text", text: content.text });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else if (updateType === "plan") {
|
|
453
|
+
const entries = (u as { entries?: Array<{ content: string; priority: string; status: string }> }).entries;
|
|
454
|
+
if (entries) {
|
|
455
|
+
latestPlan = entries;
|
|
410
456
|
}
|
|
411
457
|
} else if (updateType === "tool_call" || updateType === "tool_call_update") {
|
|
458
|
+
const toolCallId = u.toolCallId as string | undefined;
|
|
412
459
|
const status = u.status as string | undefined;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
460
|
+
const meta = u._meta as { claudeCode?: { toolName?: string } } | undefined;
|
|
461
|
+
|
|
462
|
+
// Cache tool info from initial tool_call events
|
|
463
|
+
if (updateType === "tool_call" && toolCallId) {
|
|
464
|
+
toolInfoCache.set(toolCallId, {
|
|
465
|
+
title: u.title as string | undefined,
|
|
466
|
+
name: meta?.claudeCode?.toolName,
|
|
418
467
|
input: u.rawInput,
|
|
419
|
-
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (status === "completed" || status === "failed") {
|
|
472
|
+
// Merge cached info for tool_call_update events that lack title/input
|
|
473
|
+
const cached = toolCallId ? toolInfoCache.get(toolCallId) : undefined;
|
|
474
|
+
buffer.parts.push({
|
|
475
|
+
type: "tool",
|
|
476
|
+
toolCallId,
|
|
477
|
+
title: u.title ?? cached?.title,
|
|
478
|
+
name: meta?.claudeCode?.toolName ?? cached?.name,
|
|
479
|
+
status: u.status,
|
|
480
|
+
input: u.rawInput ?? cached?.input,
|
|
481
|
+
output: extractToolOutput(u.rawOutput),
|
|
420
482
|
});
|
|
421
483
|
}
|
|
422
484
|
}
|
|
@@ -426,15 +488,24 @@ export class ACPOverMAPHandler {
|
|
|
426
488
|
updateCount++;
|
|
427
489
|
}
|
|
428
490
|
|
|
429
|
-
|
|
491
|
+
// Check if the loop ended because of cancellation
|
|
492
|
+
const wasCancelled = streamState.abortController.signal.aborted;
|
|
493
|
+
const stopReason = wasCancelled ? "cancelled" : "end_turn";
|
|
494
|
+
|
|
495
|
+
console.error(`[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates, stopReason=${stopReason}`);
|
|
430
496
|
|
|
431
497
|
// Persist conversation turns for history
|
|
432
498
|
this.recordPromptTurns(sessionId as ACPSessionId, agentId, messageContent, buffer);
|
|
433
499
|
|
|
500
|
+
// Persist latest plan in EventStore for history loading across restarts
|
|
501
|
+
if (latestPlan) {
|
|
502
|
+
this.eventStore.updateAgentPlan(agentId as AgentId, latestPlan);
|
|
503
|
+
}
|
|
504
|
+
|
|
434
505
|
// Emit updated session info after prompt completes
|
|
435
506
|
this.emitSessionInfo(streamState, sessionId, emitNotification);
|
|
436
507
|
|
|
437
|
-
return { stopReason
|
|
508
|
+
return { stopReason };
|
|
438
509
|
} catch (error) {
|
|
439
510
|
console.error(`[ACP-over-MAP] Prompt error:`, error);
|
|
440
511
|
return {
|
|
@@ -455,29 +526,33 @@ export class ACPOverMAPHandler {
|
|
|
455
526
|
// Prefer server's resolved session ID over client's potentially stale one
|
|
456
527
|
const sessionId = streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
|
|
457
528
|
|
|
458
|
-
|
|
459
|
-
|
|
529
|
+
const agentId = sessionId
|
|
530
|
+
? this.sessionMapper.getAgentId(sessionId as ACPSessionId)
|
|
531
|
+
: streamState.agentId;
|
|
460
532
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
533
|
+
console.error(
|
|
534
|
+
`[ACP-over-MAP] Cancel - streamId=${streamState.streamId} sessionId=${sessionId} agentId=${agentId}`,
|
|
535
|
+
);
|
|
464
536
|
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
if (!agentId) {
|
|
468
|
-
return { cancelled: true };
|
|
469
|
-
}
|
|
537
|
+
// 1. Abort the for-await loop in handlePrompt so it stops yielding updates
|
|
538
|
+
streamState.abortController.abort();
|
|
470
539
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
540
|
+
// 2. Cancel the agent's active session (sends session/cancel to the subprocess)
|
|
541
|
+
// This does NOT terminate the agent — it only interrupts the current prompt.
|
|
542
|
+
// The subprocess stays alive and can accept new prompts.
|
|
543
|
+
// Use map/agents/stop for full agent termination.
|
|
544
|
+
if (agentId) {
|
|
545
|
+
const session = this.agentManager.getSession(agentId);
|
|
546
|
+
if (session) {
|
|
547
|
+
try {
|
|
548
|
+
await session.cancel();
|
|
549
|
+
console.error(`[ACP-over-MAP] Session cancelled for agent ${agentId}`);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.warn(`[ACP-over-MAP] session.cancel() failed for ${agentId}:`, error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
476
554
|
}
|
|
477
555
|
|
|
478
|
-
// Clean up
|
|
479
|
-
this.sessionMapper.removeMapping(sessionId as ACPSessionId);
|
|
480
|
-
|
|
481
556
|
return { cancelled: true };
|
|
482
557
|
}
|
|
483
558
|
|
|
@@ -605,6 +680,11 @@ export class ACPOverMAPHandler {
|
|
|
605
680
|
order: "asc",
|
|
606
681
|
limit: limit ?? 200,
|
|
607
682
|
});
|
|
683
|
+
|
|
684
|
+
// Include persisted plan and agent cwd if available
|
|
685
|
+
const agent = historyAgentId ? this.eventStore.getAgent(historyAgentId as AgentId) : undefined;
|
|
686
|
+
const plan = agent?.plan ?? [];
|
|
687
|
+
|
|
608
688
|
return {
|
|
609
689
|
turns: turns.map((turn) => ({
|
|
610
690
|
role:
|
|
@@ -614,6 +694,8 @@ export class ACPOverMAPHandler {
|
|
|
614
694
|
timestamp: turn.timestamp,
|
|
615
695
|
content: turn.content,
|
|
616
696
|
})),
|
|
697
|
+
plan,
|
|
698
|
+
cwd: agent?.cwd ?? null,
|
|
617
699
|
};
|
|
618
700
|
}
|
|
619
701
|
|
|
@@ -668,7 +750,7 @@ export class ACPOverMAPHandler {
|
|
|
668
750
|
acpSessionId: ACPSessionId,
|
|
669
751
|
agentId: AgentId,
|
|
670
752
|
userMessage: string,
|
|
671
|
-
buffer: {
|
|
753
|
+
buffer: { parts: Array<{ type: "text"; text: string } | ({ type: "tool" } & Record<string, unknown>)> },
|
|
672
754
|
): void {
|
|
673
755
|
const now = Date.now();
|
|
674
756
|
|
|
@@ -691,17 +773,8 @@ export class ACPOverMAPHandler {
|
|
|
691
773
|
});
|
|
692
774
|
}
|
|
693
775
|
|
|
694
|
-
// Record assistant turn with accumulated content
|
|
695
|
-
const
|
|
696
|
-
const parts: unknown[] = [];
|
|
697
|
-
|
|
698
|
-
if (assistantText) {
|
|
699
|
-
parts.push({ type: "text", text: assistantText });
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
for (const tool of buffer.toolCalls) {
|
|
703
|
-
parts.push({ type: "tool", ...tool });
|
|
704
|
-
}
|
|
776
|
+
// Record assistant turn with accumulated content (parts already in order)
|
|
777
|
+
const parts = buffer.parts;
|
|
705
778
|
|
|
706
779
|
if (parts.length > 0) {
|
|
707
780
|
this.eventStore.emit({
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
type ScopeId,
|
|
66
66
|
} from "../types.js";
|
|
67
67
|
import type { AgentId } from "../../store/types/index.js";
|
|
68
|
+
import type { AgentStopReason } from "../../agent/types.js";
|
|
68
69
|
import {
|
|
69
70
|
createConnectionManager,
|
|
70
71
|
type ConnectionManager,
|
|
@@ -712,6 +713,10 @@ export class MAPAdapterImpl implements MAPAdapter {
|
|
|
712
713
|
"map/agents/get": async (params) =>
|
|
713
714
|
this.handleGetAgent(participantId, params),
|
|
714
715
|
|
|
716
|
+
// Agent lifecycle
|
|
717
|
+
"map/agents/stop": async (params) =>
|
|
718
|
+
this.handleStopAgent(participantId, params),
|
|
719
|
+
|
|
715
720
|
// Scope queries
|
|
716
721
|
"map/scopes/list": async () => this.handleListScopes(participantId),
|
|
717
722
|
"map/scopes/get": async (params) =>
|
|
@@ -764,6 +769,7 @@ export class MAPAdapterImpl implements MAPAdapter {
|
|
|
764
769
|
"map/send": "canMessage",
|
|
765
770
|
"map/agents/list": "canQuery",
|
|
766
771
|
"map/agents/get": "canQuery",
|
|
772
|
+
"map/agents/stop": "canStop",
|
|
767
773
|
"map/scopes/list": "canQuery",
|
|
768
774
|
"map/scopes/get": "canQuery",
|
|
769
775
|
"map/scopes/create": "canManageScopes",
|
|
@@ -808,16 +814,27 @@ export class MAPAdapterImpl implements MAPAdapter {
|
|
|
808
814
|
break;
|
|
809
815
|
}
|
|
810
816
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
817
|
+
// Process messages concurrently (same pattern as toad reference).
|
|
818
|
+
// The message loop must not block on any handler, otherwise
|
|
819
|
+
// session/cancel can't be delivered while session/prompt is running.
|
|
820
|
+
// The stream writer queues writes safely, so concurrent responses
|
|
821
|
+
// don't interleave.
|
|
822
|
+
const processAndRespond = async () => {
|
|
823
|
+
const result = await session.rpcHandler.process(value, {
|
|
824
|
+
participantId: session.participantId,
|
|
825
|
+
capabilities: participant.capabilities,
|
|
826
|
+
signal: session.abortController.signal,
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
if (result.type === "response") {
|
|
830
|
+
await this.sendToSession(session, result.response);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
processAndRespond().catch((err) => {
|
|
834
|
+
if (!session.abortController.signal.aborted) {
|
|
835
|
+
console.error(`[MAPAdapter] Error processing message:`, err);
|
|
836
|
+
}
|
|
815
837
|
});
|
|
816
|
-
|
|
817
|
-
// Send response if needed
|
|
818
|
-
if (result.type === "response") {
|
|
819
|
-
await this.sendToSession(session, result.response);
|
|
820
|
-
}
|
|
821
838
|
}
|
|
822
839
|
} catch (error) {
|
|
823
840
|
if (!session.abortController.signal.aborted) {
|
|
@@ -1181,6 +1198,59 @@ export class MAPAdapterImpl implements MAPAdapter {
|
|
|
1181
1198
|
return { agent: agent ?? null };
|
|
1182
1199
|
}
|
|
1183
1200
|
|
|
1201
|
+
private async handleStopAgent(
|
|
1202
|
+
_participantId: ParticipantId,
|
|
1203
|
+
params: unknown,
|
|
1204
|
+
): Promise<{ stopping: boolean; agent?: AgentInfo }> {
|
|
1205
|
+
const { agentId, reason, force } = (params as {
|
|
1206
|
+
agentId?: AgentId;
|
|
1207
|
+
reason?: string;
|
|
1208
|
+
force?: boolean;
|
|
1209
|
+
}) ?? {};
|
|
1210
|
+
|
|
1211
|
+
if (!agentId) {
|
|
1212
|
+
throw RPCError.invalidParams("agentId required");
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (!this.services.agentManager) {
|
|
1216
|
+
throw RPCError.internalError("Agent manager not available");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Abort any active ACP streams for this agent
|
|
1220
|
+
if (this.acpOverMapHandler) {
|
|
1221
|
+
this.acpOverMapHandler.abortStreamsForAgent(agentId);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Terminate the agent
|
|
1225
|
+
try {
|
|
1226
|
+
await this.services.agentManager.terminate(
|
|
1227
|
+
agentId,
|
|
1228
|
+
(reason ?? "cancelled") as AgentStopReason,
|
|
1229
|
+
);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
console.error(`[MAPAdapter] Error stopping agent ${agentId}:`, error);
|
|
1232
|
+
throw RPCError.internalError(
|
|
1233
|
+
`Failed to stop agent: ${error instanceof Error ? error.message : String(error)}`,
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Emit agent state changed event for subscribers
|
|
1238
|
+
this.emitEvent({
|
|
1239
|
+
eventId: `stop-${Date.now()}`,
|
|
1240
|
+
type: "agent.state.changed" as MAPEventType,
|
|
1241
|
+
timestamp: Date.now(),
|
|
1242
|
+
agentId,
|
|
1243
|
+
data: {
|
|
1244
|
+
agentId,
|
|
1245
|
+
current: "stopped",
|
|
1246
|
+
previous: "running",
|
|
1247
|
+
reason: reason ?? "cancelled",
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
return { stopping: true };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1184
1254
|
private async handleListScopes(
|
|
1185
1255
|
participantId: ParticipantId,
|
|
1186
1256
|
): Promise<{ scopes: ScopeInfo[] }> {
|
|
@@ -1056,6 +1056,46 @@ describe('Event Archival', () => {
|
|
|
1056
1056
|
await store1.close();
|
|
1057
1057
|
await store2.close();
|
|
1058
1058
|
});
|
|
1059
|
+
|
|
1060
|
+
it('should preserve agent plan across reload (rebuildViews)', async () => {
|
|
1061
|
+
const planStore = await createEventStore({
|
|
1062
|
+
baseDir: testDir,
|
|
1063
|
+
instanceId: 'plan-reload-test',
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// Spawn an agent
|
|
1067
|
+
planStore.emit({
|
|
1068
|
+
type: 'spawn',
|
|
1069
|
+
source: { agent_id: 'agent_plan' },
|
|
1070
|
+
payload: {
|
|
1071
|
+
agent_id: 'agent_plan',
|
|
1072
|
+
session_id: 'sess_plan',
|
|
1073
|
+
task: 'test plan persistence',
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Set plan via updateAgentPlan
|
|
1078
|
+
const plan = [
|
|
1079
|
+
{ content: 'Read files', priority: 'high', status: 'completed' },
|
|
1080
|
+
{ content: 'Write code', priority: 'medium', status: 'in_progress' },
|
|
1081
|
+
];
|
|
1082
|
+
planStore.updateAgentPlan('agent_plan' as AgentId, plan);
|
|
1083
|
+
|
|
1084
|
+
// Verify plan is set
|
|
1085
|
+
const before = planStore.getAgent('agent_plan' as AgentId);
|
|
1086
|
+
expect(before?.plan).toEqual(plan);
|
|
1087
|
+
|
|
1088
|
+
// Persist, then reload (simulates server restart: load from SQLite + rebuildViews)
|
|
1089
|
+
await planStore.persist();
|
|
1090
|
+
await planStore.reload();
|
|
1091
|
+
|
|
1092
|
+
// Plan should survive the reload
|
|
1093
|
+
const after = planStore.getAgent('agent_plan' as AgentId);
|
|
1094
|
+
expect(after).toBeDefined();
|
|
1095
|
+
expect(after?.plan).toEqual(plan);
|
|
1096
|
+
|
|
1097
|
+
await planStore.close();
|
|
1098
|
+
});
|
|
1059
1099
|
});
|
|
1060
1100
|
});
|
|
1061
1101
|
|
package/src/store/event-store.ts
CHANGED
|
@@ -203,6 +203,7 @@ export interface EventStore {
|
|
|
203
203
|
// Agent view
|
|
204
204
|
getAgent(agentId: AgentId): Agent | null;
|
|
205
205
|
listAgents(filter?: { state?: AgentState; parent?: AgentId | null }): Agent[];
|
|
206
|
+
updateAgentPlan(agentId: AgentId, plan: Array<{ content: string; priority: string; status: string }>): void;
|
|
206
207
|
|
|
207
208
|
// Task view
|
|
208
209
|
getTask(taskId: TaskId): Task | null;
|
|
@@ -519,6 +520,25 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
|
|
|
519
520
|
return agents;
|
|
520
521
|
}
|
|
521
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Update an agent's plan entries (persisted to SQLite via TinyBase)
|
|
525
|
+
*/
|
|
526
|
+
function updateAgentPlan(
|
|
527
|
+
agentId: AgentId,
|
|
528
|
+
plan: Array<{ content: string; priority: string; status: string }>,
|
|
529
|
+
): void {
|
|
530
|
+
const row = store.getRow('agents', agentId);
|
|
531
|
+
if (!row.id) return;
|
|
532
|
+
|
|
533
|
+
store.setPartialRow('agents', agentId, {
|
|
534
|
+
plan: JSON.stringify(plan),
|
|
535
|
+
last_activity_at: Date.now(),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const agent = rowToAgent(store.getRow('agents', agentId));
|
|
539
|
+
notifyAgentChange(agentId, agent);
|
|
540
|
+
}
|
|
541
|
+
|
|
522
542
|
/**
|
|
523
543
|
* Get task by ID
|
|
524
544
|
*/
|
|
@@ -1203,6 +1223,7 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
|
|
|
1203
1223
|
// Views
|
|
1204
1224
|
getAgent,
|
|
1205
1225
|
listAgents,
|
|
1226
|
+
updateAgentPlan,
|
|
1206
1227
|
getTask,
|
|
1207
1228
|
listTasks,
|
|
1208
1229
|
getMessages,
|
|
@@ -1269,6 +1290,17 @@ function initializeTables(store: Store): void {
|
|
|
1269
1290
|
* Rebuild materialized views from the event log
|
|
1270
1291
|
*/
|
|
1271
1292
|
function rebuildViews(store: Store): void {
|
|
1293
|
+
// Preserve out-of-band agent fields that aren't derived from events.
|
|
1294
|
+
// `plan` is written directly via updateAgentPlan(), not through events,
|
|
1295
|
+
// so it would be lost when we clear and replay.
|
|
1296
|
+
const savedAgentPlan = new Map<string, string>();
|
|
1297
|
+
for (const rowId of store.getRowIds('agents')) {
|
|
1298
|
+
const row = store.getRow('agents', rowId);
|
|
1299
|
+
if (row.plan && row.plan !== '[]') {
|
|
1300
|
+
savedAgentPlan.set(rowId, row.plan as string);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1272
1304
|
// Clear existing views
|
|
1273
1305
|
for (const rowId of store.getRowIds('agents')) {
|
|
1274
1306
|
store.delRow('agents', rowId);
|
|
@@ -1324,6 +1356,14 @@ function rebuildViews(store: Store): void {
|
|
|
1324
1356
|
for (const event of events) {
|
|
1325
1357
|
applyEventToViews(store, event, noop, noop, noop, noop, noop, noop);
|
|
1326
1358
|
}
|
|
1359
|
+
|
|
1360
|
+
// Restore out-of-band agent fields preserved before the wipe
|
|
1361
|
+
for (const [agentId, plan] of savedAgentPlan) {
|
|
1362
|
+
const row = store.getRow('agents', agentId);
|
|
1363
|
+
if (row.id) {
|
|
1364
|
+
store.setPartialRow('agents', agentId, { plan });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1327
1367
|
}
|
|
1328
1368
|
|
|
1329
1369
|
/**
|
|
@@ -1422,6 +1462,7 @@ function applySpawnEvent(
|
|
|
1422
1462
|
role: payload.role ?? '',
|
|
1423
1463
|
config: JSON.stringify(payload.config ?? {}),
|
|
1424
1464
|
cwd: payload.cwd ?? process.cwd(),
|
|
1465
|
+
plan: '[]',
|
|
1425
1466
|
created_at: event.timestamp,
|
|
1426
1467
|
started_at: 0,
|
|
1427
1468
|
stopped_at: 0,
|
|
@@ -1770,6 +1811,7 @@ function rowToAgent(row: Record<string, unknown>): Agent {
|
|
|
1770
1811
|
role: (row.role as string) || undefined,
|
|
1771
1812
|
config: row.config ? JSON.parse(row.config as string) : {},
|
|
1772
1813
|
cwd: (row.cwd as string) || process.cwd(),
|
|
1814
|
+
plan: row.plan ? JSON.parse(row.plan as string) : [],
|
|
1773
1815
|
created_at: row.created_at as Timestamp,
|
|
1774
1816
|
started_at: (row.started_at as number) || undefined,
|
|
1775
1817
|
stopped_at: (row.stopped_at as number) || undefined,
|