@clinebot/core 0.0.11 → 0.0.13
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 +1 -1
- package/dist/agents/agent-config-loader.d.ts +1 -1
- package/dist/agents/agent-config-parser.d.ts +5 -2
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/plugin-config-loader.d.ts +4 -0
- package/dist/agents/plugin-loader.d.ts +1 -0
- package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
- package/dist/agents/plugin-sandbox.d.ts +4 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +685 -413
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/session/default-session-manager.d.ts +5 -0
- package/dist/session/session-config-builder.d.ts +4 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +12 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/session/utils/types.d.ts +9 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +15 -14
- package/dist/types/config.d.ts +5 -0
- package/dist/types/events.d.ts +22 -0
- package/package.json +5 -4
- package/src/agents/agent-config-loader.test.ts +2 -0
- package/src/agents/agent-config-loader.ts +1 -0
- package/src/agents/agent-config-parser.ts +12 -5
- package/src/agents/index.ts +1 -0
- package/src/agents/plugin-config-loader.test.ts +49 -0
- package/src/agents/plugin-config-loader.ts +10 -73
- package/src/agents/plugin-loader.test.ts +127 -1
- package/src/agents/plugin-loader.ts +72 -5
- package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
- package/src/agents/plugin-sandbox.test.ts +198 -1
- package/src/agents/plugin-sandbox.ts +223 -353
- package/src/index.node.ts +14 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/index.ts +10 -0
- package/src/runtime/runtime-builder.test.ts +67 -0
- package/src/runtime/runtime-builder.ts +70 -16
- package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
- package/src/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +52 -33
- package/src/session/default-session-manager.test.ts +453 -1
- package/src/session/default-session-manager.ts +210 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-service.ts +127 -64
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +159 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/session/utils/types.ts +10 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +137 -8
- package/src/tools/definitions.ts +115 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +28 -28
- package/src/types/config.ts +5 -0
- package/src/types/events.ts +23 -0
|
@@ -11,12 +11,13 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { AgentResult } from "@clinebot/agents";
|
|
13
13
|
import type { LlmsProviders } from "@clinebot/llms";
|
|
14
|
+
import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
|
|
14
15
|
import { nanoid } from "nanoid";
|
|
15
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
17
|
import type { SessionSource, SessionStatus } from "../types/common";
|
|
17
18
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
18
19
|
import type { SessionManifest } from "./session-manifest";
|
|
19
|
-
import type { RootSessionArtifacts,
|
|
20
|
+
import type { RootSessionArtifacts, SessionRow } from "./session-service";
|
|
20
21
|
|
|
21
22
|
function nowIso(): string {
|
|
22
23
|
return new Date().toISOString();
|
|
@@ -46,7 +47,7 @@ function createResult(overrides: Partial<AgentResult> = {}): AgentResult {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
class LocalFileSessionService {
|
|
49
|
-
private readonly rows = new Map<string,
|
|
50
|
+
private readonly rows = new Map<string, SessionRow>();
|
|
50
51
|
|
|
51
52
|
constructor(private readonly sessionsDir: string) {}
|
|
52
53
|
|
|
@@ -114,33 +115,33 @@ class LocalFileSessionService {
|
|
|
114
115
|
);
|
|
115
116
|
|
|
116
117
|
this.rows.set(sessionId, {
|
|
117
|
-
|
|
118
|
+
sessionId,
|
|
118
119
|
source: input.source,
|
|
119
120
|
pid: input.pid,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
startedAt,
|
|
122
|
+
endedAt: null,
|
|
123
|
+
exitCode: null,
|
|
123
124
|
status: "running",
|
|
124
|
-
|
|
125
|
-
interactive: input.interactive
|
|
125
|
+
statusLock: 0,
|
|
126
|
+
interactive: input.interactive,
|
|
126
127
|
provider: input.provider,
|
|
127
128
|
model: input.model,
|
|
128
129
|
cwd: input.cwd,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
workspaceRoot: input.workspaceRoot,
|
|
131
|
+
teamName: input.teamName ?? null,
|
|
132
|
+
enableTools: input.enableTools,
|
|
133
|
+
enableSpawn: input.enableSpawn,
|
|
134
|
+
enableTeams: input.enableTeams,
|
|
135
|
+
parentSessionId: null,
|
|
136
|
+
parentAgentId: null,
|
|
137
|
+
agentId: null,
|
|
138
|
+
conversationId: null,
|
|
139
|
+
isSubagent: false,
|
|
139
140
|
prompt: prompt ?? null,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
transcriptPath,
|
|
142
|
+
hookPath,
|
|
143
|
+
messagesPath,
|
|
144
|
+
updatedAt: startedAt,
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
return {
|
|
@@ -158,7 +159,7 @@ class LocalFileSessionService {
|
|
|
158
159
|
systemPrompt?: string,
|
|
159
160
|
): void {
|
|
160
161
|
const row = this.rows.get(sessionId);
|
|
161
|
-
if (!row?.
|
|
162
|
+
if (!row?.messagesPath) {
|
|
162
163
|
throw new Error(`session not found: ${sessionId}`);
|
|
163
164
|
}
|
|
164
165
|
const payload: {
|
|
@@ -171,7 +172,7 @@ class LocalFileSessionService {
|
|
|
171
172
|
payload.systemPrompt = systemPrompt;
|
|
172
173
|
}
|
|
173
174
|
writeFileSync(
|
|
174
|
-
row.
|
|
175
|
+
row.messagesPath,
|
|
175
176
|
`${JSON.stringify(payload, null, 2)}\n`,
|
|
176
177
|
"utf8",
|
|
177
178
|
);
|
|
@@ -188,10 +189,10 @@ class LocalFileSessionService {
|
|
|
188
189
|
}
|
|
189
190
|
const endedAt = nowIso();
|
|
190
191
|
row.status = status;
|
|
191
|
-
row.
|
|
192
|
-
row.
|
|
193
|
-
row.
|
|
194
|
-
row.
|
|
192
|
+
row.endedAt = endedAt;
|
|
193
|
+
row.exitCode = typeof exitCode === "number" ? exitCode : null;
|
|
194
|
+
row.updatedAt = endedAt;
|
|
195
|
+
row.statusLock = row.statusLock + 1;
|
|
195
196
|
return { updated: true, endedAt };
|
|
196
197
|
}
|
|
197
198
|
|
|
@@ -203,7 +204,7 @@ class LocalFileSessionService {
|
|
|
203
204
|
);
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
listSessions(limit = 200):
|
|
207
|
+
listSessions(limit = 200): SessionRow[] {
|
|
207
208
|
return Array.from(this.rows.values()).slice(0, limit);
|
|
208
209
|
}
|
|
209
210
|
|
|
@@ -213,21 +214,39 @@ class LocalFileSessionService {
|
|
|
213
214
|
return { deleted: false };
|
|
214
215
|
}
|
|
215
216
|
this.rows.delete(sessionId);
|
|
216
|
-
unlinkSync(row.
|
|
217
|
-
unlinkSync(row.
|
|
218
|
-
unlinkSync(row.
|
|
217
|
+
unlinkSync(row.transcriptPath);
|
|
218
|
+
unlinkSync(row.hookPath);
|
|
219
|
+
unlinkSync(row.messagesPath ?? "");
|
|
219
220
|
unlinkSync(join(this.sessionsDir, sessionId, `${sessionId}.json`));
|
|
220
221
|
return { deleted: true };
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
describe("DefaultSessionManager e2e", () => {
|
|
226
|
+
const envSnapshot = {
|
|
227
|
+
HOME: process.env.HOME,
|
|
228
|
+
CLINE_DIR: process.env.CLINE_DIR,
|
|
229
|
+
};
|
|
225
230
|
const tempDirs: string[] = [];
|
|
231
|
+
let isolatedHomeDir = "";
|
|
232
|
+
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
|
|
235
|
+
process.env.HOME = isolatedHomeDir;
|
|
236
|
+
process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
|
|
237
|
+
setHomeDir(isolatedHomeDir);
|
|
238
|
+
setClineDir(process.env.CLINE_DIR);
|
|
239
|
+
});
|
|
226
240
|
|
|
227
241
|
afterEach(() => {
|
|
242
|
+
process.env.HOME = envSnapshot.HOME;
|
|
243
|
+
process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
|
|
244
|
+
setHomeDir(envSnapshot.HOME ?? "~");
|
|
245
|
+
setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
|
|
228
246
|
for (const dir of tempDirs.splice(0)) {
|
|
229
247
|
rmSync(dir, { recursive: true, force: true });
|
|
230
248
|
}
|
|
249
|
+
rmSync(isolatedHomeDir, { recursive: true, force: true });
|
|
231
250
|
});
|
|
232
251
|
|
|
233
252
|
it("runs an interactive lifecycle with real artifact files", async () => {
|
|
@@ -2,7 +2,8 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { AgentResult } from "@clinebot/agents";
|
|
5
|
-
import {
|
|
5
|
+
import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
7
|
import { TelemetryService } from "../telemetry/TelemetryService";
|
|
7
8
|
import { SessionSource } from "../types/common";
|
|
8
9
|
import type { CoreSessionConfig } from "../types/config";
|
|
@@ -55,6 +56,61 @@ function createManifest(sessionId: string): SessionManifest {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
type PluginEventTestHarness = {
|
|
60
|
+
handlePluginEvent: (
|
|
61
|
+
rootSessionId: string,
|
|
62
|
+
event: { name: string; payload?: unknown },
|
|
63
|
+
) => Promise<void>;
|
|
64
|
+
getPendingPrompts: (
|
|
65
|
+
sessionId: string,
|
|
66
|
+
) => Array<{ prompt: string; delivery: "queue" | "steer" }>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function createPluginEventHarness(
|
|
70
|
+
manager: DefaultSessionManager,
|
|
71
|
+
): PluginEventTestHarness {
|
|
72
|
+
const target = manager as object;
|
|
73
|
+
return {
|
|
74
|
+
handlePluginEvent: async (rootSessionId, event) => {
|
|
75
|
+
const handler = Reflect.get(target, "handlePluginEvent");
|
|
76
|
+
if (typeof handler !== "function") {
|
|
77
|
+
throw new Error("handlePluginEvent test hook unavailable");
|
|
78
|
+
}
|
|
79
|
+
await Reflect.apply(
|
|
80
|
+
handler as (
|
|
81
|
+
rootSessionId: string,
|
|
82
|
+
event: { name: string; payload?: unknown },
|
|
83
|
+
) => Promise<void>,
|
|
84
|
+
target,
|
|
85
|
+
[rootSessionId, event],
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
getPendingPrompts: (sessionId) => {
|
|
89
|
+
const getter = Reflect.get(target, "getSessionOrThrow");
|
|
90
|
+
if (typeof getter !== "function") {
|
|
91
|
+
throw new Error("getSessionOrThrow test hook unavailable");
|
|
92
|
+
}
|
|
93
|
+
const session = Reflect.apply(
|
|
94
|
+
getter as (sessionId: string) => {
|
|
95
|
+
pendingPrompts: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
prompt: string;
|
|
98
|
+
delivery: "queue" | "steer";
|
|
99
|
+
userFiles?: unknown;
|
|
100
|
+
userImages?: unknown;
|
|
101
|
+
}>;
|
|
102
|
+
},
|
|
103
|
+
target,
|
|
104
|
+
[sessionId],
|
|
105
|
+
);
|
|
106
|
+
return session.pendingPrompts.map(({ prompt, delivery }) => ({
|
|
107
|
+
prompt,
|
|
108
|
+
delivery,
|
|
109
|
+
}));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
58
114
|
function createConfig(
|
|
59
115
|
overrides: Partial<CoreSessionConfig> = {},
|
|
60
116
|
): CoreSessionConfig {
|
|
@@ -71,6 +127,28 @@ function createConfig(
|
|
|
71
127
|
}
|
|
72
128
|
|
|
73
129
|
describe("DefaultSessionManager", () => {
|
|
130
|
+
const envSnapshot = {
|
|
131
|
+
HOME: process.env.HOME,
|
|
132
|
+
CLINE_DIR: process.env.CLINE_DIR,
|
|
133
|
+
};
|
|
134
|
+
let isolatedHomeDir = "";
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
isolatedHomeDir = mkdtempSync(join(tmpdir(), "core-session-home-"));
|
|
138
|
+
process.env.HOME = isolatedHomeDir;
|
|
139
|
+
process.env.CLINE_DIR = join(isolatedHomeDir, ".cline");
|
|
140
|
+
setHomeDir(isolatedHomeDir);
|
|
141
|
+
setClineDir(process.env.CLINE_DIR);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
process.env.HOME = envSnapshot.HOME;
|
|
146
|
+
process.env.CLINE_DIR = envSnapshot.CLINE_DIR;
|
|
147
|
+
setHomeDir(envSnapshot.HOME ?? "~");
|
|
148
|
+
setClineDir(envSnapshot.CLINE_DIR ?? join("~", ".cline"));
|
|
149
|
+
rmSync(isolatedHomeDir, { recursive: true, force: true });
|
|
150
|
+
});
|
|
151
|
+
|
|
74
152
|
it("emits session lifecycle telemetry when configured", async () => {
|
|
75
153
|
const sessionId = "sess-telemetry";
|
|
76
154
|
const manifest = createManifest(sessionId);
|
|
@@ -309,6 +387,163 @@ describe("DefaultSessionManager", () => {
|
|
|
309
387
|
});
|
|
310
388
|
});
|
|
311
389
|
|
|
390
|
+
it("queues sandbox steer messages back into the active session", async () => {
|
|
391
|
+
const sessionId = "sess-steer";
|
|
392
|
+
const manifest = createManifest(sessionId);
|
|
393
|
+
const sessionService = {
|
|
394
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
395
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
396
|
+
manifestPath: "/tmp/manifest.json",
|
|
397
|
+
transcriptPath: "/tmp/transcript.log",
|
|
398
|
+
hookPath: "/tmp/hook.log",
|
|
399
|
+
messagesPath: "/tmp/messages.json",
|
|
400
|
+
manifest,
|
|
401
|
+
}),
|
|
402
|
+
persistSessionMessages: vi.fn(),
|
|
403
|
+
updateSessionStatus: vi.fn().mockResolvedValue({
|
|
404
|
+
updated: true,
|
|
405
|
+
endedAt: "2026-01-01T00:00:05.000Z",
|
|
406
|
+
}),
|
|
407
|
+
writeSessionManifest: vi.fn(),
|
|
408
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
409
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
410
|
+
};
|
|
411
|
+
const runtimeBuilder = {
|
|
412
|
+
build: vi.fn().mockReturnValue({
|
|
413
|
+
tools: [],
|
|
414
|
+
shutdown: vi.fn(),
|
|
415
|
+
}),
|
|
416
|
+
};
|
|
417
|
+
const run = vi.fn().mockResolvedValue(
|
|
418
|
+
createResult({
|
|
419
|
+
messages: [
|
|
420
|
+
{ role: "user", content: [{ type: "text", text: "hello" }] },
|
|
421
|
+
],
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
const continueFn = vi.fn().mockResolvedValue(
|
|
425
|
+
createResult({
|
|
426
|
+
text: "steered",
|
|
427
|
+
messages: [
|
|
428
|
+
{ role: "user", content: [{ type: "text", text: "hello" }] },
|
|
429
|
+
{
|
|
430
|
+
role: "assistant",
|
|
431
|
+
content: [{ type: "text", text: "steered" }],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
const agent = {
|
|
437
|
+
run,
|
|
438
|
+
continue: continueFn,
|
|
439
|
+
abort: vi.fn(),
|
|
440
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
441
|
+
getMessages: vi
|
|
442
|
+
.fn()
|
|
443
|
+
.mockReturnValue([
|
|
444
|
+
{ role: "user", content: [{ type: "text", text: "hello" }] },
|
|
445
|
+
]),
|
|
446
|
+
canStartRun: vi.fn().mockReturnValue(true),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const manager = new DefaultSessionManager({
|
|
450
|
+
distinctId,
|
|
451
|
+
sessionService: sessionService as never,
|
|
452
|
+
runtimeBuilder,
|
|
453
|
+
createAgent: () => agent as never,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await manager.start({
|
|
457
|
+
config: createConfig({ sessionId }),
|
|
458
|
+
prompt: "hello",
|
|
459
|
+
interactive: true,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const harness = createPluginEventHarness(manager);
|
|
463
|
+
await harness.handlePluginEvent(sessionId, {
|
|
464
|
+
name: "steer_message",
|
|
465
|
+
payload: { prompt: "async result" },
|
|
466
|
+
});
|
|
467
|
+
await vi.waitFor(() => {
|
|
468
|
+
expect(continueFn).toHaveBeenCalledTimes(2);
|
|
469
|
+
});
|
|
470
|
+
expect(continueFn).toHaveBeenLastCalledWith(
|
|
471
|
+
'<user_input mode="act">async result</user_input>',
|
|
472
|
+
undefined,
|
|
473
|
+
undefined,
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("promotes queued prompts to the front when they become steer", async () => {
|
|
478
|
+
const sessionId = "sess-steer-priority";
|
|
479
|
+
const manifest = createManifest(sessionId);
|
|
480
|
+
const sessionService = {
|
|
481
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
482
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
483
|
+
manifestPath: "/tmp/manifest.json",
|
|
484
|
+
transcriptPath: "/tmp/transcript.log",
|
|
485
|
+
hookPath: "/tmp/hook.log",
|
|
486
|
+
messagesPath: "/tmp/messages.json",
|
|
487
|
+
manifest,
|
|
488
|
+
}),
|
|
489
|
+
persistSessionMessages: vi.fn(),
|
|
490
|
+
updateSessionStatus: vi.fn().mockResolvedValue({
|
|
491
|
+
updated: true,
|
|
492
|
+
endedAt: "2026-01-01T00:00:05.000Z",
|
|
493
|
+
}),
|
|
494
|
+
writeSessionManifest: vi.fn(),
|
|
495
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
496
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
497
|
+
};
|
|
498
|
+
const runtimeBuilder = {
|
|
499
|
+
build: vi.fn().mockReturnValue({
|
|
500
|
+
tools: [],
|
|
501
|
+
shutdown: vi.fn(),
|
|
502
|
+
}),
|
|
503
|
+
};
|
|
504
|
+
const agent = {
|
|
505
|
+
run: vi.fn().mockResolvedValue(createResult()),
|
|
506
|
+
continue: vi.fn().mockResolvedValue(createResult()),
|
|
507
|
+
abort: vi.fn(),
|
|
508
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
509
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
510
|
+
canStartRun: vi.fn().mockReturnValue(false),
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const manager = new DefaultSessionManager({
|
|
514
|
+
distinctId,
|
|
515
|
+
sessionService: sessionService as never,
|
|
516
|
+
runtimeBuilder,
|
|
517
|
+
createAgent: () => agent as never,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await manager.start({
|
|
521
|
+
config: createConfig({ sessionId }),
|
|
522
|
+
prompt: "hello",
|
|
523
|
+
interactive: true,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const harness = createPluginEventHarness(manager);
|
|
527
|
+
|
|
528
|
+
await harness.handlePluginEvent(sessionId, {
|
|
529
|
+
name: "queue_message",
|
|
530
|
+
payload: { prompt: "queued first" },
|
|
531
|
+
});
|
|
532
|
+
await harness.handlePluginEvent(sessionId, {
|
|
533
|
+
name: "queue_message",
|
|
534
|
+
payload: { prompt: "queued second" },
|
|
535
|
+
});
|
|
536
|
+
await harness.handlePluginEvent(sessionId, {
|
|
537
|
+
name: "steer_message",
|
|
538
|
+
payload: { prompt: "queued first" },
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
expect(harness.getPendingPrompts(sessionId)).toEqual([
|
|
542
|
+
{ prompt: "queued first", delivery: "steer" },
|
|
543
|
+
{ prompt: "queued second", delivery: "queue" },
|
|
544
|
+
]);
|
|
545
|
+
});
|
|
546
|
+
|
|
312
547
|
it("preserves per-turn metadata on prior assistant messages across turns", async () => {
|
|
313
548
|
const sessionId = "sess-meta-multi";
|
|
314
549
|
const manifest = createManifest(sessionId);
|
|
@@ -653,6 +888,112 @@ describe("DefaultSessionManager", () => {
|
|
|
653
888
|
});
|
|
654
889
|
});
|
|
655
890
|
|
|
891
|
+
it("queues sends with explicit queue or steer delivery and emits snapshots", async () => {
|
|
892
|
+
const sessionId = "sess-delivery-queue";
|
|
893
|
+
const manifest = createManifest(sessionId);
|
|
894
|
+
const sessionService = {
|
|
895
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
896
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
897
|
+
manifestPath: "/tmp/manifest-queue.json",
|
|
898
|
+
transcriptPath: "/tmp/transcript-queue.log",
|
|
899
|
+
hookPath: "/tmp/hook-queue.log",
|
|
900
|
+
messagesPath: "/tmp/messages-queue.json",
|
|
901
|
+
manifest,
|
|
902
|
+
}),
|
|
903
|
+
persistSessionMessages: vi.fn(),
|
|
904
|
+
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
|
|
905
|
+
writeSessionManifest: vi.fn(),
|
|
906
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
907
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
908
|
+
};
|
|
909
|
+
const runtimeBuilder = {
|
|
910
|
+
build: vi.fn().mockReturnValue({
|
|
911
|
+
tools: [],
|
|
912
|
+
shutdown: vi.fn(),
|
|
913
|
+
}),
|
|
914
|
+
};
|
|
915
|
+
let canStartRun = false;
|
|
916
|
+
const run = vi.fn().mockResolvedValue(createResult({ text: "first" }));
|
|
917
|
+
const continueFn = vi
|
|
918
|
+
.fn()
|
|
919
|
+
.mockResolvedValue(createResult({ text: "next" }));
|
|
920
|
+
const manager = new DefaultSessionManager({
|
|
921
|
+
distinctId,
|
|
922
|
+
sessionService: sessionService as never,
|
|
923
|
+
runtimeBuilder,
|
|
924
|
+
createAgent: () =>
|
|
925
|
+
({
|
|
926
|
+
run,
|
|
927
|
+
continue: continueFn,
|
|
928
|
+
canStartRun: vi.fn(() => canStartRun),
|
|
929
|
+
abort: vi.fn(),
|
|
930
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
931
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
932
|
+
messages: [],
|
|
933
|
+
}) as never,
|
|
934
|
+
});
|
|
935
|
+
const events: Array<unknown> = [];
|
|
936
|
+
manager.subscribe((event) => {
|
|
937
|
+
events.push(event);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
await manager.start({
|
|
941
|
+
config: createConfig({ sessionId }),
|
|
942
|
+
interactive: true,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
await expect(
|
|
946
|
+
manager.send({ sessionId, prompt: "queued first", delivery: "queue" }),
|
|
947
|
+
).resolves.toBeUndefined();
|
|
948
|
+
await expect(
|
|
949
|
+
manager.send({ sessionId, prompt: "queued second", delivery: "steer" }),
|
|
950
|
+
).resolves.toBeUndefined();
|
|
951
|
+
|
|
952
|
+
expect(run).not.toHaveBeenCalled();
|
|
953
|
+
expect(continueFn).not.toHaveBeenCalled();
|
|
954
|
+
const promptSnapshots = events
|
|
955
|
+
.filter((event) => {
|
|
956
|
+
return (
|
|
957
|
+
typeof event === "object" &&
|
|
958
|
+
event !== null &&
|
|
959
|
+
"type" in event &&
|
|
960
|
+
event.type === "pending_prompts"
|
|
961
|
+
);
|
|
962
|
+
})
|
|
963
|
+
.map((event) => (event as { payload: { prompts: unknown[] } }).payload);
|
|
964
|
+
expect(promptSnapshots.at(-1)).toEqual({
|
|
965
|
+
prompts: [
|
|
966
|
+
expect.objectContaining({
|
|
967
|
+
prompt: "queued second",
|
|
968
|
+
delivery: "steer",
|
|
969
|
+
attachmentCount: 0,
|
|
970
|
+
}),
|
|
971
|
+
expect.objectContaining({
|
|
972
|
+
prompt: "queued first",
|
|
973
|
+
delivery: "queue",
|
|
974
|
+
attachmentCount: 0,
|
|
975
|
+
}),
|
|
976
|
+
],
|
|
977
|
+
sessionId,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
canStartRun = true;
|
|
981
|
+
await manager.send({ sessionId, prompt: "run now" });
|
|
982
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
983
|
+
expect(
|
|
984
|
+
events.some((event) => {
|
|
985
|
+
return (
|
|
986
|
+
typeof event === "object" &&
|
|
987
|
+
event !== null &&
|
|
988
|
+
"type" in event &&
|
|
989
|
+
event.type === "pending_prompt_submitted" &&
|
|
990
|
+
"payload" in event &&
|
|
991
|
+
(event.payload as { prompt?: string }).prompt === "queued second"
|
|
992
|
+
);
|
|
993
|
+
}),
|
|
994
|
+
).toBe(true);
|
|
995
|
+
});
|
|
996
|
+
|
|
656
997
|
it("returns undefined accumulated usage for unknown sessions", async () => {
|
|
657
998
|
const manager = new DefaultSessionManager({
|
|
658
999
|
distinctId,
|
|
@@ -1286,4 +1627,115 @@ describe("DefaultSessionManager", () => {
|
|
|
1286
1627
|
failedMessages,
|
|
1287
1628
|
);
|
|
1288
1629
|
});
|
|
1630
|
+
|
|
1631
|
+
it("persists teammate progress updates for team-task sub-sessions", async () => {
|
|
1632
|
+
const sessionId = "sess-team-task-progress";
|
|
1633
|
+
const manifest = createManifest(sessionId);
|
|
1634
|
+
const sessionService = {
|
|
1635
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
1636
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
1637
|
+
manifestPath: "/tmp/manifest-team-task-progress.json",
|
|
1638
|
+
transcriptPath: "/tmp/transcript-team-task-progress.log",
|
|
1639
|
+
hookPath: "/tmp/hook-team-task-progress.log",
|
|
1640
|
+
messagesPath: "/tmp/messages-team-task-progress.json",
|
|
1641
|
+
manifest,
|
|
1642
|
+
}),
|
|
1643
|
+
persistSessionMessages: vi.fn(),
|
|
1644
|
+
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
|
|
1645
|
+
writeSessionManifest: vi.fn(),
|
|
1646
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
1647
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
1648
|
+
onTeamTaskStart: vi.fn().mockResolvedValue(undefined),
|
|
1649
|
+
onTeamTaskEnd: vi.fn().mockResolvedValue(undefined),
|
|
1650
|
+
onTeamTaskProgress: vi.fn().mockResolvedValue(undefined),
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
let onTeamEvent: ((event: unknown) => void) | undefined;
|
|
1654
|
+
const runtimeBuilder = {
|
|
1655
|
+
build: vi
|
|
1656
|
+
.fn()
|
|
1657
|
+
.mockImplementation(
|
|
1658
|
+
(input: { onTeamEvent?: (event: unknown) => void }) => {
|
|
1659
|
+
onTeamEvent = input.onTeamEvent;
|
|
1660
|
+
return {
|
|
1661
|
+
tools: [],
|
|
1662
|
+
shutdown: vi.fn(),
|
|
1663
|
+
};
|
|
1664
|
+
},
|
|
1665
|
+
),
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
const manager = new DefaultSessionManager({
|
|
1669
|
+
distinctId,
|
|
1670
|
+
sessionService: sessionService as never,
|
|
1671
|
+
runtimeBuilder,
|
|
1672
|
+
createAgent: () =>
|
|
1673
|
+
({
|
|
1674
|
+
run: vi.fn().mockImplementation(async () => {
|
|
1675
|
+
onTeamEvent?.({
|
|
1676
|
+
type: "task_start",
|
|
1677
|
+
agentId: "providers-investigator",
|
|
1678
|
+
message: "Investigate provider boundaries",
|
|
1679
|
+
});
|
|
1680
|
+
onTeamEvent?.({
|
|
1681
|
+
type: "run_progress",
|
|
1682
|
+
run: {
|
|
1683
|
+
id: "run_00002",
|
|
1684
|
+
agentId: "providers-investigator",
|
|
1685
|
+
status: "running",
|
|
1686
|
+
message: "Investigate provider boundaries",
|
|
1687
|
+
priority: 0,
|
|
1688
|
+
retryCount: 0,
|
|
1689
|
+
maxRetries: 0,
|
|
1690
|
+
continueConversation: false,
|
|
1691
|
+
startedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
1692
|
+
lastProgressAt: new Date("2026-01-01T00:00:01.000Z"),
|
|
1693
|
+
lastProgressMessage: "heartbeat",
|
|
1694
|
+
currentActivity: "heartbeat",
|
|
1695
|
+
},
|
|
1696
|
+
message: "heartbeat",
|
|
1697
|
+
});
|
|
1698
|
+
onTeamEvent?.({
|
|
1699
|
+
type: "agent_event",
|
|
1700
|
+
agentId: "providers-investigator",
|
|
1701
|
+
event: {
|
|
1702
|
+
type: "content_start",
|
|
1703
|
+
contentType: "text",
|
|
1704
|
+
text: "Drafting the provider boundary analysis now.",
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
onTeamEvent?.({
|
|
1708
|
+
type: "task_end",
|
|
1709
|
+
agentId: "providers-investigator",
|
|
1710
|
+
result: createResult(),
|
|
1711
|
+
});
|
|
1712
|
+
return createResult({ text: "lead handled progress" });
|
|
1713
|
+
}),
|
|
1714
|
+
continue: vi.fn(),
|
|
1715
|
+
abort: vi.fn(),
|
|
1716
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
1717
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
1718
|
+
messages: [],
|
|
1719
|
+
}) as never,
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
await manager.start({
|
|
1723
|
+
config: createConfig({ sessionId }),
|
|
1724
|
+
prompt: "run teammate work",
|
|
1725
|
+
interactive: false,
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
|
|
1729
|
+
sessionId,
|
|
1730
|
+
"providers-investigator",
|
|
1731
|
+
"heartbeat",
|
|
1732
|
+
{ kind: "heartbeat" },
|
|
1733
|
+
);
|
|
1734
|
+
expect(sessionService.onTeamTaskProgress).toHaveBeenCalledWith(
|
|
1735
|
+
sessionId,
|
|
1736
|
+
"providers-investigator",
|
|
1737
|
+
"Drafting the provider boundary analysis now.",
|
|
1738
|
+
{ kind: "text" },
|
|
1739
|
+
);
|
|
1740
|
+
});
|
|
1289
1741
|
});
|