@pickle-pee/runtime 0.0.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 +99 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/kernel-session-adapter.d.ts +73 -0
- package/dist/adapters/kernel-session-adapter.js +10 -0
- package/dist/adapters/kernel-session-adapter.js.map +1 -0
- package/dist/adapters/pi-mono-event-bridge.d.ts +54 -0
- package/dist/adapters/pi-mono-event-bridge.js +159 -0
- package/dist/adapters/pi-mono-event-bridge.js.map +1 -0
- package/dist/adapters/pi-mono-session-adapter.d.ts +75 -0
- package/dist/adapters/pi-mono-session-adapter.js +490 -0
- package/dist/adapters/pi-mono-session-adapter.js.map +1 -0
- package/dist/create-app-runtime.d.ts +52 -0
- package/dist/create-app-runtime.js +163 -0
- package/dist/create-app-runtime.js.map +1 -0
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.js +5 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/events/event-bus.d.ts +23 -0
- package/dist/events/event-bus.js +85 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +6 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/runtime-event.d.ts +158 -0
- package/dist/events/runtime-event.js +13 -0
- package/dist/events/runtime-event.js.map +1 -0
- package/dist/governance/tool-governor.d.ts +63 -0
- package/dist/governance/tool-governor.js +639 -0
- package/dist/governance/tool-governor.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/planning/index.d.ts +6 -0
- package/dist/planning/index.js +16 -0
- package/dist/planning/index.js.map +1 -0
- package/dist/planning/plan-engine.d.ts +49 -0
- package/dist/planning/plan-engine.js +174 -0
- package/dist/planning/plan-engine.js.map +1 -0
- package/dist/planning/plan-events.d.ts +14 -0
- package/dist/planning/plan-events.js +94 -0
- package/dist/planning/plan-events.js.map +1 -0
- package/dist/planning/plan-orchestrator.d.ts +56 -0
- package/dist/planning/plan-orchestrator.js +167 -0
- package/dist/planning/plan-orchestrator.js.map +1 -0
- package/dist/planning/plan-types.d.ts +36 -0
- package/dist/planning/plan-types.js +9 -0
- package/dist/planning/plan-types.js.map +1 -0
- package/dist/runtime-context.d.ts +21 -0
- package/dist/runtime-context.js +37 -0
- package/dist/runtime-context.js.map +1 -0
- package/dist/services/event-normalizer.d.ts +22 -0
- package/dist/services/event-normalizer.js +162 -0
- package/dist/services/event-normalizer.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/session/session-events.d.ts +11 -0
- package/dist/session/session-events.js +52 -0
- package/dist/session/session-events.js.map +1 -0
- package/dist/session/session-facade.d.ts +88 -0
- package/dist/session/session-facade.js +439 -0
- package/dist/session/session-facade.js.map +1 -0
- package/dist/session/session-state.d.ts +14 -0
- package/dist/session/session-state.js +75 -0
- package/dist/session/session-state.js.map +1 -0
- package/dist/subagent/aggregation.d.ts +25 -0
- package/dist/subagent/aggregation.js +124 -0
- package/dist/subagent/aggregation.js.map +1 -0
- package/dist/subagent/index.d.ts +10 -0
- package/dist/subagent/index.js +29 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/path-scope.d.ts +24 -0
- package/dist/subagent/path-scope.js +86 -0
- package/dist/subagent/path-scope.js.map +1 -0
- package/dist/subagent/result-types.d.ts +61 -0
- package/dist/subagent/result-types.js +9 -0
- package/dist/subagent/result-types.js.map +1 -0
- package/dist/subagent/stop-condition.d.ts +34 -0
- package/dist/subagent/stop-condition.js +76 -0
- package/dist/subagent/stop-condition.js.map +1 -0
- package/dist/subagent/task-types.d.ts +48 -0
- package/dist/subagent/task-types.js +10 -0
- package/dist/subagent/task-types.js.map +1 -0
- package/dist/subagent/task-validator.d.ts +22 -0
- package/dist/subagent/task-validator.js +79 -0
- package/dist/subagent/task-validator.js.map +1 -0
- package/dist/subagent/verification.d.ts +22 -0
- package/dist/subagent/verification.js +55 -0
- package/dist/subagent/verification.js.map +1 -0
- package/dist/test/aggregation.test.d.ts +1 -0
- package/dist/test/aggregation.test.js +201 -0
- package/dist/test/aggregation.test.js.map +1 -0
- package/dist/test/create-app-runtime.test.d.ts +1 -0
- package/dist/test/create-app-runtime.test.js +286 -0
- package/dist/test/create-app-runtime.test.js.map +1 -0
- package/dist/test/event-bus.test.d.ts +1 -0
- package/dist/test/event-bus.test.js +81 -0
- package/dist/test/event-bus.test.js.map +1 -0
- package/dist/test/event-normalizer.test.d.ts +1 -0
- package/dist/test/event-normalizer.test.js +143 -0
- package/dist/test/event-normalizer.test.js.map +1 -0
- package/dist/test/path-scope.test.d.ts +1 -0
- package/dist/test/path-scope.test.js +71 -0
- package/dist/test/path-scope.test.js.map +1 -0
- package/dist/test/pi-mono-event-bridge.test.d.ts +1 -0
- package/dist/test/pi-mono-event-bridge.test.js +125 -0
- package/dist/test/pi-mono-event-bridge.test.js.map +1 -0
- package/dist/test/pi-mono-live.test.d.ts +1 -0
- package/dist/test/pi-mono-live.test.js +289 -0
- package/dist/test/pi-mono-live.test.js.map +1 -0
- package/dist/test/pi-mono-session-adapter.test.d.ts +1 -0
- package/dist/test/pi-mono-session-adapter.test.js +260 -0
- package/dist/test/pi-mono-session-adapter.test.js.map +1 -0
- package/dist/test/plan-engine.test.d.ts +1 -0
- package/dist/test/plan-engine.test.js +235 -0
- package/dist/test/plan-engine.test.js.map +1 -0
- package/dist/test/plan-events.test.d.ts +1 -0
- package/dist/test/plan-events.test.js +81 -0
- package/dist/test/plan-events.test.js.map +1 -0
- package/dist/test/plan-orchestrator.test.d.ts +1 -0
- package/dist/test/plan-orchestrator.test.js +324 -0
- package/dist/test/plan-orchestrator.test.js.map +1 -0
- package/dist/test/runtime-context.test.d.ts +1 -0
- package/dist/test/runtime-context.test.js +70 -0
- package/dist/test/runtime-context.test.js.map +1 -0
- package/dist/test/session-facade.test.d.ts +1 -0
- package/dist/test/session-facade.test.js +1011 -0
- package/dist/test/session-facade.test.js.map +1 -0
- package/dist/test/session-state.test.d.ts +1 -0
- package/dist/test/session-state.test.js +118 -0
- package/dist/test/session-state.test.js.map +1 -0
- package/dist/test/stop-condition.test.d.ts +1 -0
- package/dist/test/stop-condition.test.js +105 -0
- package/dist/test/stop-condition.test.js.map +1 -0
- package/dist/test/stubs/stub-kernel-session-adapter.d.ts +45 -0
- package/dist/test/stubs/stub-kernel-session-adapter.js +186 -0
- package/dist/test/stubs/stub-kernel-session-adapter.js.map +1 -0
- package/dist/test/task-validator.test.d.ts +1 -0
- package/dist/test/task-validator.test.js +97 -0
- package/dist/test/task-validator.test.js.map +1 -0
- package/dist/test/tool-governor.test.d.ts +1 -0
- package/dist/test/tool-governor.test.js +379 -0
- package/dist/test/tool-governor.test.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const create_app_runtime_js_1 = require("../create-app-runtime.js");
|
|
5
|
+
const event_bus_js_1 = require("../events/event-bus.js");
|
|
6
|
+
const tool_governor_js_1 = require("../governance/tool-governor.js");
|
|
7
|
+
const plan_engine_js_1 = require("../planning/plan-engine.js");
|
|
8
|
+
const runtime_context_js_1 = require("../runtime-context.js");
|
|
9
|
+
const session_facade_js_1 = require("../session/session-facade.js");
|
|
10
|
+
const session_state_js_1 = require("../session/session-state.js");
|
|
11
|
+
const stub_kernel_session_adapter_js_1 = require("./stubs/stub-kernel-session-adapter.js");
|
|
12
|
+
const stubId = { value: "facade-test" };
|
|
13
|
+
const stubModel = { id: "test-model", provider: "test" };
|
|
14
|
+
(0, vitest_1.describe)("SessionFacade", () => {
|
|
15
|
+
function createFacade() {
|
|
16
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
17
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
18
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
19
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
20
|
+
sessionId: stubId,
|
|
21
|
+
workingDirectory: "/tmp",
|
|
22
|
+
mode: "print",
|
|
23
|
+
model: stubModel,
|
|
24
|
+
toolSet: new Set(["read"]),
|
|
25
|
+
});
|
|
26
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus);
|
|
27
|
+
return { facade, adapter, globalBus };
|
|
28
|
+
}
|
|
29
|
+
(0, vitest_1.it)("transitions to active on construction", () => {
|
|
30
|
+
const { facade } = createFacade();
|
|
31
|
+
(0, vitest_1.expect)(facade.state.status).toBe("active");
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)("exposes session id", () => {
|
|
34
|
+
const { facade } = createFacade();
|
|
35
|
+
(0, vitest_1.expect)(facade.id).toEqual(stubId);
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)("exposes runtime context", () => {
|
|
38
|
+
const { facade } = createFacade();
|
|
39
|
+
(0, vitest_1.expect)(facade.context.mode).toBe("print");
|
|
40
|
+
(0, vitest_1.expect)(facade.context.workingDirectory).toBe("/tmp");
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.it)("processes prompt and emits normalized events", async () => {
|
|
43
|
+
const { facade, adapter, globalBus } = createFacade();
|
|
44
|
+
const globalEvents = [];
|
|
45
|
+
globalBus.onCategory("tool", (event) => globalEvents.push(event));
|
|
46
|
+
adapter.enqueueDefaultEvents([
|
|
47
|
+
{
|
|
48
|
+
type: "tool_execution_start",
|
|
49
|
+
timestamp: 1000,
|
|
50
|
+
payload: { toolName: "read", toolCallId: "c1", parameters: {} },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "tool_execution_end",
|
|
54
|
+
timestamp: 2000,
|
|
55
|
+
payload: { toolName: "read", toolCallId: "c1", status: "success", durationMs: 100 },
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
await facade.prompt("Read the file");
|
|
59
|
+
(0, vitest_1.expect)(globalEvents).toHaveLength(2);
|
|
60
|
+
(0, vitest_1.expect)(globalEvents[0].type).toBe("tool_started");
|
|
61
|
+
(0, vitest_1.expect)(globalEvents[1].type).toBe("tool_completed");
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)("updates task state during prompt", async () => {
|
|
64
|
+
const { facade, adapter } = createFacade();
|
|
65
|
+
adapter.enqueueDefaultEvents([{ type: "message_update", timestamp: 1000, payload: { content: "hi" } }]);
|
|
66
|
+
const stateHistory = [];
|
|
67
|
+
facade.onStateChange((s) => stateHistory.push(s.taskState.status));
|
|
68
|
+
await facade.prompt("test");
|
|
69
|
+
// Should have transitioned: idle → running → idle
|
|
70
|
+
(0, vitest_1.expect)(stateHistory).toContain("running");
|
|
71
|
+
(0, vitest_1.expect)(stateHistory.at(-1)).toBe("idle");
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("session events bus receives events", async () => {
|
|
74
|
+
const { facade, adapter } = createFacade();
|
|
75
|
+
const sessionEvents = [];
|
|
76
|
+
facade.events.on("text_delta", (e) => sessionEvents.push(e));
|
|
77
|
+
adapter.enqueueDefaultEvents([{ type: "message_update", timestamp: 1000, payload: { content: "Hello" } }]);
|
|
78
|
+
await facade.prompt("test");
|
|
79
|
+
(0, vitest_1.expect)(sessionEvents).toHaveLength(1);
|
|
80
|
+
if (sessionEvents[0].type === "text_delta") {
|
|
81
|
+
(0, vitest_1.expect)(sessionEvents[0].content).toBe("Hello");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("emits compaction events when compact() is invoked", async () => {
|
|
85
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
86
|
+
const runtime = (0, create_app_runtime_js_1.createAppRuntime)({
|
|
87
|
+
workingDirectory: "/tmp",
|
|
88
|
+
mode: "interactive",
|
|
89
|
+
model: { id: "stub-model", provider: "stub" },
|
|
90
|
+
toolSet: ["read", "edit"],
|
|
91
|
+
adapter,
|
|
92
|
+
});
|
|
93
|
+
const session = runtime.createSession();
|
|
94
|
+
const seen = [];
|
|
95
|
+
session.events.onAny((event) => {
|
|
96
|
+
seen.push(event.type);
|
|
97
|
+
});
|
|
98
|
+
await session.compact();
|
|
99
|
+
(0, vitest_1.expect)(seen).toContain("compaction_started");
|
|
100
|
+
(0, vitest_1.expect)(seen).toContain("compaction_completed");
|
|
101
|
+
await session.close();
|
|
102
|
+
await runtime.shutdown();
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("close emits session_closed event", async () => {
|
|
105
|
+
const { facade, globalBus } = createFacade();
|
|
106
|
+
let closed = false;
|
|
107
|
+
globalBus.on("session_closed", () => {
|
|
108
|
+
closed = true;
|
|
109
|
+
});
|
|
110
|
+
await facade.close();
|
|
111
|
+
(0, vitest_1.expect)(closed).toBe(true);
|
|
112
|
+
(0, vitest_1.expect)(facade.state.status).toBe("closed");
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)("close calls adapter close", async () => {
|
|
115
|
+
const { facade, adapter } = createFacade();
|
|
116
|
+
await facade.close();
|
|
117
|
+
(0, vitest_1.expect)(adapter.closed).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)("prompt throws after close", async () => {
|
|
120
|
+
const { facade } = createFacade();
|
|
121
|
+
await facade.close();
|
|
122
|
+
await (0, vitest_1.expect)(facade.prompt("test")).rejects.toThrow("Session is closed");
|
|
123
|
+
});
|
|
124
|
+
(0, vitest_1.it)("abort calls adapter abort for an active stream", async () => {
|
|
125
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
126
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
127
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
128
|
+
sessionId: stubId,
|
|
129
|
+
workingDirectory: "/tmp",
|
|
130
|
+
mode: "print",
|
|
131
|
+
model: stubModel,
|
|
132
|
+
toolSet: new Set(["read"]),
|
|
133
|
+
});
|
|
134
|
+
let releaseStream;
|
|
135
|
+
const streamBlocked = new Promise((r) => {
|
|
136
|
+
releaseStream = r;
|
|
137
|
+
});
|
|
138
|
+
let abortCalled = false;
|
|
139
|
+
const slowAdapter = {
|
|
140
|
+
async *sendPrompt(_input) {
|
|
141
|
+
await streamBlocked;
|
|
142
|
+
if (!abortCalled) {
|
|
143
|
+
yield {
|
|
144
|
+
type: "message_update",
|
|
145
|
+
timestamp: 1000,
|
|
146
|
+
payload: { content: "after abort" },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
async *sendContinue(_input) {
|
|
151
|
+
await streamBlocked;
|
|
152
|
+
if (!abortCalled) {
|
|
153
|
+
yield {
|
|
154
|
+
type: "message_update",
|
|
155
|
+
timestamp: 1000,
|
|
156
|
+
payload: { content: "" },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
abort() {
|
|
161
|
+
abortCalled = true;
|
|
162
|
+
},
|
|
163
|
+
async close() { },
|
|
164
|
+
getRecoveryData() {
|
|
165
|
+
return {
|
|
166
|
+
sessionId: stubId,
|
|
167
|
+
model: stubModel,
|
|
168
|
+
toolSet: ["read"],
|
|
169
|
+
planSummary: null,
|
|
170
|
+
compactionSummary: null,
|
|
171
|
+
taskState: { status: "idle", currentTaskId: null, startedAt: null },
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
resume() { },
|
|
175
|
+
};
|
|
176
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(slowAdapter, state, context, globalBus);
|
|
177
|
+
const prompt = facade.prompt("test");
|
|
178
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
179
|
+
facade.abort();
|
|
180
|
+
(0, vitest_1.expect)(abortCalled).toBe(true);
|
|
181
|
+
releaseStream();
|
|
182
|
+
await prompt;
|
|
183
|
+
});
|
|
184
|
+
(0, vitest_1.it)("onStateChange notifies listeners", async () => {
|
|
185
|
+
const { facade, adapter } = createFacade();
|
|
186
|
+
adapter.enqueueDefaultEvents([]);
|
|
187
|
+
const states = [];
|
|
188
|
+
facade.onStateChange((s) => states.push(s.taskState.status));
|
|
189
|
+
await facade.prompt("test");
|
|
190
|
+
(0, vitest_1.expect)(states.length).toBeGreaterThanOrEqual(2);
|
|
191
|
+
(0, vitest_1.expect)(states).toContain("running");
|
|
192
|
+
});
|
|
193
|
+
// --- Fix 3: RuntimeContext.taskState sync ---
|
|
194
|
+
(0, vitest_1.it)("updates context.taskState during prompt", async () => {
|
|
195
|
+
const { facade, adapter } = createFacade();
|
|
196
|
+
adapter.enqueueDefaultEvents([{ type: "message_update", timestamp: 1000, payload: { content: "hi" } }]);
|
|
197
|
+
const contextStates = [];
|
|
198
|
+
facade.onStateChange(() => contextStates.push(facade.context.taskState.status));
|
|
199
|
+
await facade.prompt("test");
|
|
200
|
+
(0, vitest_1.expect)(contextStates).toContain("running");
|
|
201
|
+
(0, vitest_1.expect)(contextStates.at(-1)).toBe("idle");
|
|
202
|
+
});
|
|
203
|
+
// --- Fix 2: Concurrency guard ---
|
|
204
|
+
(0, vitest_1.it)("rejects concurrent prompt calls", async () => {
|
|
205
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
206
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
207
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
208
|
+
sessionId: stubId,
|
|
209
|
+
workingDirectory: "/tmp",
|
|
210
|
+
mode: "print",
|
|
211
|
+
model: stubModel,
|
|
212
|
+
toolSet: new Set(["read"]),
|
|
213
|
+
});
|
|
214
|
+
let releaseStream;
|
|
215
|
+
const streamBlocked = new Promise((r) => {
|
|
216
|
+
releaseStream = r;
|
|
217
|
+
});
|
|
218
|
+
const slowAdapter = {
|
|
219
|
+
async *sendPrompt(_input) {
|
|
220
|
+
await streamBlocked;
|
|
221
|
+
yield { type: "message_update", timestamp: 1000, payload: { content: "hi" } };
|
|
222
|
+
},
|
|
223
|
+
async *sendContinue(_input) {
|
|
224
|
+
await streamBlocked;
|
|
225
|
+
yield { type: "message_update", timestamp: 1000, payload: { content: "" } };
|
|
226
|
+
},
|
|
227
|
+
abort() { },
|
|
228
|
+
async close() { },
|
|
229
|
+
getRecoveryData() {
|
|
230
|
+
return {
|
|
231
|
+
sessionId: stubId,
|
|
232
|
+
model: stubModel,
|
|
233
|
+
toolSet: ["read"],
|
|
234
|
+
planSummary: null,
|
|
235
|
+
compactionSummary: null,
|
|
236
|
+
taskState: { status: "idle", currentTaskId: null, startedAt: null },
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
resume() { },
|
|
240
|
+
};
|
|
241
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(slowAdapter, state, context, globalBus);
|
|
242
|
+
const first = facade.prompt("first");
|
|
243
|
+
// Give the event loop a tick so the first prompt enters the for-await
|
|
244
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
245
|
+
await (0, vitest_1.expect)(facade.prompt("second")).rejects.toThrow("already running");
|
|
246
|
+
releaseStream();
|
|
247
|
+
await first;
|
|
248
|
+
});
|
|
249
|
+
(0, vitest_1.it)("keeps the running lock until an aborted stream settles", async () => {
|
|
250
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
251
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
252
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
253
|
+
sessionId: stubId,
|
|
254
|
+
workingDirectory: "/tmp",
|
|
255
|
+
mode: "print",
|
|
256
|
+
model: stubModel,
|
|
257
|
+
toolSet: new Set(["read"]),
|
|
258
|
+
});
|
|
259
|
+
let releaseStream;
|
|
260
|
+
const streamBlocked = new Promise((r) => {
|
|
261
|
+
releaseStream = r;
|
|
262
|
+
});
|
|
263
|
+
let aborted = false;
|
|
264
|
+
const slowAdapter = {
|
|
265
|
+
async *sendPrompt(_input) {
|
|
266
|
+
await streamBlocked;
|
|
267
|
+
if (!aborted) {
|
|
268
|
+
yield { type: "message_update", timestamp: 1000, payload: { content: "hi" } };
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
async *sendContinue(_input) {
|
|
272
|
+
await streamBlocked;
|
|
273
|
+
if (!aborted) {
|
|
274
|
+
yield { type: "message_update", timestamp: 1000, payload: { content: "" } };
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
abort() {
|
|
278
|
+
aborted = true;
|
|
279
|
+
},
|
|
280
|
+
async close() { },
|
|
281
|
+
getRecoveryData() {
|
|
282
|
+
return {
|
|
283
|
+
sessionId: stubId,
|
|
284
|
+
model: stubModel,
|
|
285
|
+
toolSet: ["read"],
|
|
286
|
+
planSummary: null,
|
|
287
|
+
compactionSummary: null,
|
|
288
|
+
taskState: { status: "idle", currentTaskId: null, startedAt: null },
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
resume() { },
|
|
292
|
+
};
|
|
293
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(slowAdapter, state, context, globalBus);
|
|
294
|
+
const first = facade.prompt("first");
|
|
295
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
296
|
+
facade.abort();
|
|
297
|
+
await (0, vitest_1.expect)(facade.prompt("second")).rejects.toThrow("already running");
|
|
298
|
+
releaseStream();
|
|
299
|
+
await first;
|
|
300
|
+
});
|
|
301
|
+
(0, vitest_1.it)("allows sequential prompt calls", async () => {
|
|
302
|
+
const { facade, adapter } = createFacade();
|
|
303
|
+
adapter.enqueueDefaultEvents([]);
|
|
304
|
+
await facade.prompt("first");
|
|
305
|
+
await facade.prompt("second");
|
|
306
|
+
// No error thrown
|
|
307
|
+
});
|
|
308
|
+
(0, vitest_1.it)("gates tool execution before emitting tool_started when adapter supports governance hooks", async () => {
|
|
309
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
310
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
311
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["bash"]));
|
|
312
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
313
|
+
sessionId: stubId,
|
|
314
|
+
workingDirectory: "/tmp",
|
|
315
|
+
mode: "print",
|
|
316
|
+
model: stubModel,
|
|
317
|
+
toolSet: new Set(["bash"]),
|
|
318
|
+
});
|
|
319
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
320
|
+
governor.catalog.register({
|
|
321
|
+
identity: { name: "bash", category: "command-execution" },
|
|
322
|
+
contract: {
|
|
323
|
+
parameterSchema: { type: "object", properties: {} },
|
|
324
|
+
output: { type: "text" },
|
|
325
|
+
errorTypes: [],
|
|
326
|
+
},
|
|
327
|
+
policy: {
|
|
328
|
+
riskLevel: "L3",
|
|
329
|
+
readOnly: false,
|
|
330
|
+
concurrency: "unlimited",
|
|
331
|
+
confirmation: "always",
|
|
332
|
+
subAgentAllowed: true,
|
|
333
|
+
timeoutMs: 60_000,
|
|
334
|
+
},
|
|
335
|
+
executorTag: "bash",
|
|
336
|
+
});
|
|
337
|
+
adapter.enqueueDefaultEvents([
|
|
338
|
+
{
|
|
339
|
+
type: "tool_execution_start",
|
|
340
|
+
timestamp: 1000,
|
|
341
|
+
payload: {
|
|
342
|
+
toolName: "bash",
|
|
343
|
+
toolCallId: "bash_1",
|
|
344
|
+
parameters: { command: "echo hello" },
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
type: "tool_execution_end",
|
|
349
|
+
timestamp: 2000,
|
|
350
|
+
payload: { toolName: "bash", toolCallId: "bash_1", status: "success", durationMs: 20 },
|
|
351
|
+
},
|
|
352
|
+
]);
|
|
353
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
354
|
+
const received = [];
|
|
355
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
356
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
357
|
+
const prompt = facade.prompt("run bash");
|
|
358
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
359
|
+
(0, vitest_1.expect)(received).toHaveLength(1);
|
|
360
|
+
(0, vitest_1.expect)(received[0].type).toBe("permission_requested");
|
|
361
|
+
(0, vitest_1.expect)(governor.audit.size).toBe(0);
|
|
362
|
+
await facade.resolvePermission("bash_1", "deny");
|
|
363
|
+
await prompt;
|
|
364
|
+
});
|
|
365
|
+
(0, vitest_1.it)("resumes a gated tool after permission approval", async () => {
|
|
366
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
367
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
368
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["bash"]));
|
|
369
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
370
|
+
sessionId: stubId,
|
|
371
|
+
workingDirectory: "/tmp",
|
|
372
|
+
mode: "print",
|
|
373
|
+
model: stubModel,
|
|
374
|
+
toolSet: new Set(["bash"]),
|
|
375
|
+
});
|
|
376
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
377
|
+
governor.catalog.register({
|
|
378
|
+
identity: { name: "bash", category: "command-execution" },
|
|
379
|
+
contract: {
|
|
380
|
+
parameterSchema: { type: "object", properties: {} },
|
|
381
|
+
output: { type: "text" },
|
|
382
|
+
errorTypes: [],
|
|
383
|
+
},
|
|
384
|
+
policy: {
|
|
385
|
+
riskLevel: "L3",
|
|
386
|
+
readOnly: false,
|
|
387
|
+
concurrency: "unlimited",
|
|
388
|
+
confirmation: "always",
|
|
389
|
+
subAgentAllowed: true,
|
|
390
|
+
timeoutMs: 60_000,
|
|
391
|
+
},
|
|
392
|
+
executorTag: "bash",
|
|
393
|
+
});
|
|
394
|
+
adapter.enqueueDefaultEvents([
|
|
395
|
+
{
|
|
396
|
+
type: "tool_execution_start",
|
|
397
|
+
timestamp: 1000,
|
|
398
|
+
payload: {
|
|
399
|
+
toolName: "bash",
|
|
400
|
+
toolCallId: "bash_1",
|
|
401
|
+
parameters: { command: "echo hello" },
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: "tool_execution_end",
|
|
406
|
+
timestamp: 2000,
|
|
407
|
+
payload: { toolName: "bash", toolCallId: "bash_1", status: "success", durationMs: 20 },
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
411
|
+
const received = [];
|
|
412
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
413
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
414
|
+
const prompt = facade.prompt("run bash");
|
|
415
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
416
|
+
(0, vitest_1.expect)(received.map((event) => event.type)).toEqual(["permission_requested"]);
|
|
417
|
+
await facade.resolvePermission("bash_1", "allow_once");
|
|
418
|
+
await prompt;
|
|
419
|
+
(0, vitest_1.expect)(received.map((event) => event.type)).toEqual([
|
|
420
|
+
"permission_requested",
|
|
421
|
+
"permission_resolved",
|
|
422
|
+
"tool_started",
|
|
423
|
+
"tool_completed",
|
|
424
|
+
]);
|
|
425
|
+
(0, vitest_1.expect)(governor.audit.size).toBe(1);
|
|
426
|
+
});
|
|
427
|
+
(0, vitest_1.it)("emits tool_denied when a gated tool is rejected by the user", async () => {
|
|
428
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
429
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
430
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["bash"]));
|
|
431
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
432
|
+
sessionId: stubId,
|
|
433
|
+
workingDirectory: "/tmp",
|
|
434
|
+
mode: "print",
|
|
435
|
+
model: stubModel,
|
|
436
|
+
toolSet: new Set(["bash"]),
|
|
437
|
+
});
|
|
438
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
439
|
+
governor.catalog.register({
|
|
440
|
+
identity: { name: "bash", category: "command-execution" },
|
|
441
|
+
contract: {
|
|
442
|
+
parameterSchema: { type: "object", properties: {} },
|
|
443
|
+
output: { type: "text" },
|
|
444
|
+
errorTypes: [],
|
|
445
|
+
},
|
|
446
|
+
policy: {
|
|
447
|
+
riskLevel: "L3",
|
|
448
|
+
readOnly: false,
|
|
449
|
+
concurrency: "unlimited",
|
|
450
|
+
confirmation: "always",
|
|
451
|
+
subAgentAllowed: true,
|
|
452
|
+
timeoutMs: 60_000,
|
|
453
|
+
},
|
|
454
|
+
executorTag: "bash",
|
|
455
|
+
});
|
|
456
|
+
adapter.enqueueDefaultEvents([
|
|
457
|
+
{
|
|
458
|
+
type: "tool_execution_start",
|
|
459
|
+
timestamp: 1000,
|
|
460
|
+
payload: {
|
|
461
|
+
toolName: "bash",
|
|
462
|
+
toolCallId: "bash_2",
|
|
463
|
+
parameters: { command: "echo hello" },
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
type: "tool_execution_end",
|
|
468
|
+
timestamp: 2000,
|
|
469
|
+
payload: { toolName: "bash", toolCallId: "bash_2", status: "success", durationMs: 20 },
|
|
470
|
+
},
|
|
471
|
+
]);
|
|
472
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
473
|
+
const received = [];
|
|
474
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
475
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
476
|
+
const prompt = facade.prompt("run bash");
|
|
477
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
478
|
+
await facade.resolvePermission("bash_2", "deny");
|
|
479
|
+
await prompt;
|
|
480
|
+
(0, vitest_1.expect)(received.map((event) => event.type)).toEqual([
|
|
481
|
+
"permission_requested",
|
|
482
|
+
"permission_resolved",
|
|
483
|
+
"tool_denied",
|
|
484
|
+
]);
|
|
485
|
+
});
|
|
486
|
+
(0, vitest_1.it)("reuses allow_for_session approval for the same bash command within the session", async () => {
|
|
487
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
488
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
489
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["bash"]));
|
|
490
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
491
|
+
sessionId: stubId,
|
|
492
|
+
workingDirectory: "/tmp",
|
|
493
|
+
mode: "print",
|
|
494
|
+
model: stubModel,
|
|
495
|
+
toolSet: new Set(["bash"]),
|
|
496
|
+
});
|
|
497
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
498
|
+
governor.catalog.register({
|
|
499
|
+
identity: { name: "bash", category: "command-execution" },
|
|
500
|
+
contract: {
|
|
501
|
+
parameterSchema: { type: "object", properties: {} },
|
|
502
|
+
output: { type: "text" },
|
|
503
|
+
errorTypes: [],
|
|
504
|
+
},
|
|
505
|
+
policy: {
|
|
506
|
+
riskLevel: "L3",
|
|
507
|
+
readOnly: false,
|
|
508
|
+
concurrency: "unlimited",
|
|
509
|
+
confirmation: "always",
|
|
510
|
+
subAgentAllowed: true,
|
|
511
|
+
timeoutMs: 60_000,
|
|
512
|
+
},
|
|
513
|
+
executorTag: "bash",
|
|
514
|
+
});
|
|
515
|
+
adapter.enqueueEventsForPrompt("run bash once", [
|
|
516
|
+
{
|
|
517
|
+
type: "tool_execution_start",
|
|
518
|
+
timestamp: 1000,
|
|
519
|
+
payload: {
|
|
520
|
+
toolName: "bash",
|
|
521
|
+
toolCallId: "bash_pwd_1",
|
|
522
|
+
parameters: { command: "pwd" },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
type: "tool_execution_end",
|
|
527
|
+
timestamp: 2000,
|
|
528
|
+
payload: { toolName: "bash", toolCallId: "bash_pwd_1", status: "success", durationMs: 20 },
|
|
529
|
+
},
|
|
530
|
+
]);
|
|
531
|
+
adapter.enqueueEventsForPrompt("run bash twice", [
|
|
532
|
+
{
|
|
533
|
+
type: "tool_execution_start",
|
|
534
|
+
timestamp: 3000,
|
|
535
|
+
payload: {
|
|
536
|
+
toolName: "bash",
|
|
537
|
+
toolCallId: "bash_pwd_2",
|
|
538
|
+
parameters: { command: "pwd" },
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
type: "tool_execution_end",
|
|
543
|
+
timestamp: 4000,
|
|
544
|
+
payload: { toolName: "bash", toolCallId: "bash_pwd_2", status: "success", durationMs: 20 },
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
547
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
548
|
+
const received = [];
|
|
549
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
550
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
551
|
+
const firstPrompt = facade.prompt("run bash once");
|
|
552
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
553
|
+
await facade.resolvePermission("bash_pwd_1", "allow_for_session");
|
|
554
|
+
await firstPrompt;
|
|
555
|
+
const eventsAfterFirstPrompt = received.length;
|
|
556
|
+
await facade.prompt("run bash twice");
|
|
557
|
+
(0, vitest_1.expect)(received.slice(eventsAfterFirstPrompt).map((event) => event.type)).toEqual([
|
|
558
|
+
"tool_started",
|
|
559
|
+
"tool_completed",
|
|
560
|
+
]);
|
|
561
|
+
});
|
|
562
|
+
(0, vitest_1.it)("reuses allow_for_session approval for write on the same file within the session", async () => {
|
|
563
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
564
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
565
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["write"]));
|
|
566
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
567
|
+
sessionId: stubId,
|
|
568
|
+
workingDirectory: "/tmp",
|
|
569
|
+
mode: "print",
|
|
570
|
+
model: stubModel,
|
|
571
|
+
toolSet: new Set(["write"]),
|
|
572
|
+
});
|
|
573
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
574
|
+
const recordedApprovals = [];
|
|
575
|
+
const originalRecordSessionApproval = governor.recordSessionApproval.bind(governor);
|
|
576
|
+
governor.recordSessionApproval = (entry) => {
|
|
577
|
+
recordedApprovals.push(entry);
|
|
578
|
+
originalRecordSessionApproval(entry);
|
|
579
|
+
};
|
|
580
|
+
governor.catalog.register({
|
|
581
|
+
identity: { name: "write", category: "file-mutation" },
|
|
582
|
+
contract: {
|
|
583
|
+
parameterSchema: { type: "object", properties: {} },
|
|
584
|
+
output: { type: "text" },
|
|
585
|
+
errorTypes: [],
|
|
586
|
+
},
|
|
587
|
+
policy: {
|
|
588
|
+
riskLevel: "L3",
|
|
589
|
+
readOnly: false,
|
|
590
|
+
concurrency: "per_target",
|
|
591
|
+
confirmation: "always",
|
|
592
|
+
subAgentAllowed: true,
|
|
593
|
+
timeoutMs: 60_000,
|
|
594
|
+
},
|
|
595
|
+
executorTag: "write",
|
|
596
|
+
});
|
|
597
|
+
adapter.enqueueEventsForPrompt("write once", [
|
|
598
|
+
{
|
|
599
|
+
type: "tool_execution_start",
|
|
600
|
+
timestamp: 1000,
|
|
601
|
+
payload: {
|
|
602
|
+
toolName: "write",
|
|
603
|
+
toolCallId: "write_1",
|
|
604
|
+
parameters: { file_path: "/tmp/.tmp-tests/one.txt", content: "hello" },
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
type: "tool_execution_end",
|
|
609
|
+
timestamp: 2000,
|
|
610
|
+
payload: { toolName: "write", toolCallId: "write_1", status: "success", durationMs: 20 },
|
|
611
|
+
},
|
|
612
|
+
]);
|
|
613
|
+
adapter.enqueueEventsForPrompt("write twice", [
|
|
614
|
+
{
|
|
615
|
+
type: "tool_execution_start",
|
|
616
|
+
timestamp: 3000,
|
|
617
|
+
payload: {
|
|
618
|
+
toolName: "write",
|
|
619
|
+
toolCallId: "write_2",
|
|
620
|
+
parameters: { file_path: "/tmp/.tmp-tests/one.txt", content: "world" },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
type: "tool_execution_end",
|
|
625
|
+
timestamp: 4000,
|
|
626
|
+
payload: { toolName: "write", toolCallId: "write_2", status: "success", durationMs: 20 },
|
|
627
|
+
},
|
|
628
|
+
]);
|
|
629
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
630
|
+
const received = [];
|
|
631
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
632
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
633
|
+
const firstPrompt = facade.prompt("write once");
|
|
634
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
635
|
+
await facade.resolvePermission("write_1", "allow_for_session");
|
|
636
|
+
await firstPrompt;
|
|
637
|
+
(0, vitest_1.expect)(recordedApprovals[0]).toMatchObject({
|
|
638
|
+
toolName: "write",
|
|
639
|
+
riskLevel: "L3",
|
|
640
|
+
targetPattern: "/tmp/**",
|
|
641
|
+
});
|
|
642
|
+
const writeDecision = governor.beforeExecution({
|
|
643
|
+
sessionId: stubId.value,
|
|
644
|
+
toolName: "write",
|
|
645
|
+
toolCallId: "write_probe",
|
|
646
|
+
workingDirectory: "/tmp",
|
|
647
|
+
sessionMode: "print",
|
|
648
|
+
isSubAgent: false,
|
|
649
|
+
targetPath: "/tmp/.tmp-tests/one.txt",
|
|
650
|
+
parameters: { file_path: "/tmp/.tmp-tests/one.txt", content: "world" },
|
|
651
|
+
});
|
|
652
|
+
(0, vitest_1.expect)(writeDecision).toMatchObject({ type: "allow" });
|
|
653
|
+
const eventsAfterFirstPrompt = received.length;
|
|
654
|
+
await facade.prompt("write twice");
|
|
655
|
+
(0, vitest_1.expect)(received.slice(eventsAfterFirstPrompt).map((event) => event.type)).toEqual([
|
|
656
|
+
"tool_started",
|
|
657
|
+
"tool_completed",
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
(0, vitest_1.it)("reuses allow_for_session approval for edit on the same file within the session", async () => {
|
|
661
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
662
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
663
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["edit"]));
|
|
664
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
665
|
+
sessionId: stubId,
|
|
666
|
+
workingDirectory: "/tmp",
|
|
667
|
+
mode: "print",
|
|
668
|
+
model: stubModel,
|
|
669
|
+
toolSet: new Set(["edit"]),
|
|
670
|
+
});
|
|
671
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
672
|
+
governor.catalog.register({
|
|
673
|
+
identity: { name: "edit", category: "file-mutation" },
|
|
674
|
+
contract: {
|
|
675
|
+
parameterSchema: { type: "object", properties: {} },
|
|
676
|
+
output: { type: "text" },
|
|
677
|
+
errorTypes: [],
|
|
678
|
+
},
|
|
679
|
+
policy: {
|
|
680
|
+
riskLevel: "L3",
|
|
681
|
+
readOnly: false,
|
|
682
|
+
concurrency: "per_target",
|
|
683
|
+
confirmation: "always",
|
|
684
|
+
subAgentAllowed: true,
|
|
685
|
+
timeoutMs: 60_000,
|
|
686
|
+
},
|
|
687
|
+
executorTag: "edit",
|
|
688
|
+
});
|
|
689
|
+
adapter.enqueueEventsForPrompt("edit once", [
|
|
690
|
+
{
|
|
691
|
+
type: "tool_execution_start",
|
|
692
|
+
timestamp: 1000,
|
|
693
|
+
payload: {
|
|
694
|
+
toolName: "edit",
|
|
695
|
+
toolCallId: "edit_1",
|
|
696
|
+
parameters: {
|
|
697
|
+
file_path: "/tmp/.tmp-tests/two.txt",
|
|
698
|
+
old_string: "before",
|
|
699
|
+
new_string: "after",
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
type: "tool_execution_end",
|
|
705
|
+
timestamp: 2000,
|
|
706
|
+
payload: { toolName: "edit", toolCallId: "edit_1", status: "success", durationMs: 20 },
|
|
707
|
+
},
|
|
708
|
+
]);
|
|
709
|
+
adapter.enqueueEventsForPrompt("edit twice", [
|
|
710
|
+
{
|
|
711
|
+
type: "tool_execution_start",
|
|
712
|
+
timestamp: 3000,
|
|
713
|
+
payload: {
|
|
714
|
+
toolName: "edit",
|
|
715
|
+
toolCallId: "edit_2",
|
|
716
|
+
parameters: {
|
|
717
|
+
file_path: "/tmp/.tmp-tests/two.txt",
|
|
718
|
+
old_string: "after",
|
|
719
|
+
new_string: "final",
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
type: "tool_execution_end",
|
|
725
|
+
timestamp: 4000,
|
|
726
|
+
payload: { toolName: "edit", toolCallId: "edit_2", status: "success", durationMs: 20 },
|
|
727
|
+
},
|
|
728
|
+
]);
|
|
729
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
730
|
+
const received = [];
|
|
731
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
732
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
733
|
+
const firstPrompt = facade.prompt("edit once");
|
|
734
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
735
|
+
await facade.resolvePermission("edit_1", "allow_for_session");
|
|
736
|
+
await firstPrompt;
|
|
737
|
+
(0, vitest_1.expect)(governor.beforeExecution({
|
|
738
|
+
sessionId: stubId.value,
|
|
739
|
+
toolName: "edit",
|
|
740
|
+
toolCallId: "edit_probe",
|
|
741
|
+
workingDirectory: "/tmp",
|
|
742
|
+
sessionMode: "print",
|
|
743
|
+
isSubAgent: false,
|
|
744
|
+
targetPath: "/tmp/.tmp-tests/two.txt",
|
|
745
|
+
parameters: {
|
|
746
|
+
file_path: "/tmp/.tmp-tests/two.txt",
|
|
747
|
+
old_string: "after",
|
|
748
|
+
new_string: "final",
|
|
749
|
+
},
|
|
750
|
+
}).type).toBe("allow");
|
|
751
|
+
const eventsAfterFirstPrompt = received.length;
|
|
752
|
+
await facade.prompt("edit twice");
|
|
753
|
+
(0, vitest_1.expect)(received.slice(eventsAfterFirstPrompt).map((event) => event.type)).toEqual([
|
|
754
|
+
"tool_started",
|
|
755
|
+
"tool_completed",
|
|
756
|
+
]);
|
|
757
|
+
});
|
|
758
|
+
(0, vitest_1.it)("reuses allow_for_session approval for another file under the same working directory", async () => {
|
|
759
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
760
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
761
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["write"]));
|
|
762
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
763
|
+
sessionId: stubId,
|
|
764
|
+
workingDirectory: "/tmp",
|
|
765
|
+
mode: "print",
|
|
766
|
+
model: stubModel,
|
|
767
|
+
toolSet: new Set(["write"]),
|
|
768
|
+
});
|
|
769
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
770
|
+
governor.catalog.register({
|
|
771
|
+
identity: { name: "write", category: "file-mutation" },
|
|
772
|
+
contract: {
|
|
773
|
+
parameterSchema: { type: "object", properties: {} },
|
|
774
|
+
output: { type: "text" },
|
|
775
|
+
errorTypes: [],
|
|
776
|
+
},
|
|
777
|
+
policy: {
|
|
778
|
+
riskLevel: "L3",
|
|
779
|
+
readOnly: false,
|
|
780
|
+
concurrency: "per_target",
|
|
781
|
+
confirmation: "always",
|
|
782
|
+
subAgentAllowed: true,
|
|
783
|
+
timeoutMs: 60_000,
|
|
784
|
+
},
|
|
785
|
+
executorTag: "write",
|
|
786
|
+
});
|
|
787
|
+
adapter.enqueueEventsForPrompt("write first file", [
|
|
788
|
+
{
|
|
789
|
+
type: "tool_execution_start",
|
|
790
|
+
timestamp: 1000,
|
|
791
|
+
payload: {
|
|
792
|
+
toolName: "write",
|
|
793
|
+
toolCallId: "write_a",
|
|
794
|
+
parameters: { file_path: "/tmp/.tmp-tests/a.txt", content: "hello" },
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
type: "tool_execution_end",
|
|
799
|
+
timestamp: 2000,
|
|
800
|
+
payload: { toolName: "write", toolCallId: "write_a", status: "success", durationMs: 20 },
|
|
801
|
+
},
|
|
802
|
+
]);
|
|
803
|
+
adapter.enqueueEventsForPrompt("write second file", [
|
|
804
|
+
{
|
|
805
|
+
type: "tool_execution_start",
|
|
806
|
+
timestamp: 3000,
|
|
807
|
+
payload: {
|
|
808
|
+
toolName: "write",
|
|
809
|
+
toolCallId: "write_b",
|
|
810
|
+
parameters: { file_path: "/tmp/.tmp-tests/b.txt", content: "world" },
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
type: "tool_execution_end",
|
|
815
|
+
timestamp: 4000,
|
|
816
|
+
payload: { toolName: "write", toolCallId: "write_b", status: "success", durationMs: 20 },
|
|
817
|
+
},
|
|
818
|
+
]);
|
|
819
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
820
|
+
const received = [];
|
|
821
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
822
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
823
|
+
const firstPrompt = facade.prompt("write first file");
|
|
824
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
825
|
+
await facade.resolvePermission("write_a", "allow_for_session");
|
|
826
|
+
await firstPrompt;
|
|
827
|
+
(0, vitest_1.expect)(governor.beforeExecution({
|
|
828
|
+
sessionId: stubId.value,
|
|
829
|
+
toolName: "write",
|
|
830
|
+
toolCallId: "write_probe_b",
|
|
831
|
+
workingDirectory: "/tmp",
|
|
832
|
+
sessionMode: "print",
|
|
833
|
+
isSubAgent: false,
|
|
834
|
+
targetPath: "/tmp/.tmp-tests/b.txt",
|
|
835
|
+
parameters: { file_path: "/tmp/.tmp-tests/b.txt", content: "world" },
|
|
836
|
+
}).type).toBe("allow");
|
|
837
|
+
const eventsAfterFirstPrompt = received.length;
|
|
838
|
+
await facade.prompt("write second file");
|
|
839
|
+
(0, vitest_1.expect)(received.slice(eventsAfterFirstPrompt).map((event) => event.type)).toEqual([
|
|
840
|
+
"tool_started",
|
|
841
|
+
"tool_completed",
|
|
842
|
+
]);
|
|
843
|
+
});
|
|
844
|
+
(0, vitest_1.it)("asks again when the same tool targets a different directory outside the session scope", async () => {
|
|
845
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
846
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
847
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["edit"]));
|
|
848
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
849
|
+
sessionId: stubId,
|
|
850
|
+
workingDirectory: "/tmp/project",
|
|
851
|
+
mode: "print",
|
|
852
|
+
model: stubModel,
|
|
853
|
+
toolSet: new Set(["edit"]),
|
|
854
|
+
});
|
|
855
|
+
const governor = (0, tool_governor_js_1.createToolGovernor)();
|
|
856
|
+
governor.catalog.register({
|
|
857
|
+
identity: { name: "edit", category: "file-mutation" },
|
|
858
|
+
contract: {
|
|
859
|
+
parameterSchema: { type: "object", properties: {} },
|
|
860
|
+
output: { type: "text" },
|
|
861
|
+
errorTypes: [],
|
|
862
|
+
},
|
|
863
|
+
policy: {
|
|
864
|
+
riskLevel: "L3",
|
|
865
|
+
readOnly: false,
|
|
866
|
+
concurrency: "per_target",
|
|
867
|
+
confirmation: "always",
|
|
868
|
+
subAgentAllowed: true,
|
|
869
|
+
timeoutMs: 60_000,
|
|
870
|
+
},
|
|
871
|
+
executorTag: "edit",
|
|
872
|
+
});
|
|
873
|
+
adapter.enqueueEventsForPrompt("edit external one", [
|
|
874
|
+
{
|
|
875
|
+
type: "tool_execution_start",
|
|
876
|
+
timestamp: 1000,
|
|
877
|
+
payload: {
|
|
878
|
+
toolName: "edit",
|
|
879
|
+
toolCallId: "edit_external_1",
|
|
880
|
+
parameters: {
|
|
881
|
+
file_path: "/opt/tmp-tests/a.txt",
|
|
882
|
+
old_string: "before",
|
|
883
|
+
new_string: "after",
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
type: "tool_execution_end",
|
|
889
|
+
timestamp: 2000,
|
|
890
|
+
payload: { toolName: "edit", toolCallId: "edit_external_1", status: "success", durationMs: 20 },
|
|
891
|
+
},
|
|
892
|
+
]);
|
|
893
|
+
adapter.enqueueEventsForPrompt("edit external two", [
|
|
894
|
+
{
|
|
895
|
+
type: "tool_execution_start",
|
|
896
|
+
timestamp: 3000,
|
|
897
|
+
payload: {
|
|
898
|
+
toolName: "edit",
|
|
899
|
+
toolCallId: "edit_external_2",
|
|
900
|
+
parameters: {
|
|
901
|
+
file_path: "/var/tmp-tests/b.txt",
|
|
902
|
+
old_string: "before",
|
|
903
|
+
new_string: "after",
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
type: "tool_execution_end",
|
|
909
|
+
timestamp: 4000,
|
|
910
|
+
payload: { toolName: "edit", toolCallId: "edit_external_2", status: "success", durationMs: 20 },
|
|
911
|
+
},
|
|
912
|
+
]);
|
|
913
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, governor);
|
|
914
|
+
const received = [];
|
|
915
|
+
globalBus.onCategory("permission", (event) => received.push(event));
|
|
916
|
+
globalBus.onCategory("tool", (event) => received.push(event));
|
|
917
|
+
const firstPrompt = facade.prompt("edit external one");
|
|
918
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
919
|
+
await facade.resolvePermission("edit_external_1", "allow_for_session");
|
|
920
|
+
await firstPrompt;
|
|
921
|
+
const eventsAfterFirstPrompt = received.length;
|
|
922
|
+
const secondPrompt = facade.prompt("edit external two");
|
|
923
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
924
|
+
(0, vitest_1.expect)(received.slice(eventsAfterFirstPrompt).map((event) => event.type)).toEqual(["permission_requested"]);
|
|
925
|
+
await facade.resolvePermission("edit_external_2", "deny");
|
|
926
|
+
await secondPrompt;
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
// ---------------------------------------------------------------------------
|
|
930
|
+
// Plan integration tests
|
|
931
|
+
// ---------------------------------------------------------------------------
|
|
932
|
+
(0, vitest_1.describe)("SessionFacade — plan integration", () => {
|
|
933
|
+
function createFacadeWithPlan() {
|
|
934
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
935
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
936
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
937
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
938
|
+
sessionId: stubId,
|
|
939
|
+
workingDirectory: "/tmp",
|
|
940
|
+
mode: "print",
|
|
941
|
+
model: stubModel,
|
|
942
|
+
toolSet: new Set(["read"]),
|
|
943
|
+
});
|
|
944
|
+
const planEngine = (0, plan_engine_js_1.createPlanEngine)();
|
|
945
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus, undefined, planEngine);
|
|
946
|
+
return { facade, adapter, globalBus, planEngine };
|
|
947
|
+
}
|
|
948
|
+
(0, vitest_1.it)("exposes plan orchestrator when planEngine is provided", () => {
|
|
949
|
+
const { facade } = createFacadeWithPlan();
|
|
950
|
+
(0, vitest_1.expect)(facade.plan).not.toBeNull();
|
|
951
|
+
(0, vitest_1.expect)(facade.plan.engine).toBeDefined();
|
|
952
|
+
});
|
|
953
|
+
(0, vitest_1.it)("plan is null when no planEngine is provided", () => {
|
|
954
|
+
const adapter = new stub_kernel_session_adapter_js_1.StubKernelSessionAdapter();
|
|
955
|
+
const globalBus = (0, event_bus_js_1.createEventBus)();
|
|
956
|
+
const state = (0, session_state_js_1.createInitialSessionState)(stubId, stubModel, new Set(["read"]));
|
|
957
|
+
const context = (0, runtime_context_js_1.createRuntimeContext)({
|
|
958
|
+
sessionId: stubId,
|
|
959
|
+
workingDirectory: "/tmp",
|
|
960
|
+
mode: "print",
|
|
961
|
+
model: stubModel,
|
|
962
|
+
toolSet: new Set(["read"]),
|
|
963
|
+
});
|
|
964
|
+
const facade = new session_facade_js_1.SessionFacadeImpl(adapter, state, context, globalBus);
|
|
965
|
+
(0, vitest_1.expect)(facade.plan).toBeNull();
|
|
966
|
+
});
|
|
967
|
+
(0, vitest_1.it)("plan events update sessionState.planSummary", () => {
|
|
968
|
+
const { facade } = createFacadeWithPlan();
|
|
969
|
+
(0, vitest_1.expect)(facade.state.planSummary).toBeNull();
|
|
970
|
+
facade.plan.createAndActivate("p1", "Test goal", ["Step A"]);
|
|
971
|
+
// After plan creation, planSummary should be updated via event listener
|
|
972
|
+
(0, vitest_1.expect)(facade.state.planSummary).not.toBeNull();
|
|
973
|
+
(0, vitest_1.expect)(facade.state.planSummary.planId).toBe("p1");
|
|
974
|
+
(0, vitest_1.expect)(facade.state.planSummary.stepCount).toBe(1);
|
|
975
|
+
});
|
|
976
|
+
(0, vitest_1.it)("plan state changes trigger onStateChange", () => {
|
|
977
|
+
const { facade } = createFacadeWithPlan();
|
|
978
|
+
const states = [];
|
|
979
|
+
facade.onStateChange((s) => states.push(s));
|
|
980
|
+
facade.plan.createAndActivate("p1", "Goal", ["Step A"]);
|
|
981
|
+
(0, vitest_1.expect)(states.length).toBeGreaterThan(0);
|
|
982
|
+
const last = states[states.length - 1];
|
|
983
|
+
(0, vitest_1.expect)(last.planSummary).not.toBeNull();
|
|
984
|
+
});
|
|
985
|
+
(0, vitest_1.it)("plan boundary violation prevents step completion", () => {
|
|
986
|
+
const { facade } = createFacadeWithPlan();
|
|
987
|
+
facade.plan.createAndActivate("p1", "Goal", ["Step A"]);
|
|
988
|
+
facade.plan.assignTask(0, {
|
|
989
|
+
taskId: "task-1",
|
|
990
|
+
goal: "Do something",
|
|
991
|
+
scope: { allowedPaths: ["packages/app-runtime/**"], forbiddenPaths: [] },
|
|
992
|
+
inputs: { docs: [], files: [], assumptions: [] },
|
|
993
|
+
deliverables: ["code"],
|
|
994
|
+
verification: [{ name: "build", type: "command", command: "npm run build", description: "Build" }],
|
|
995
|
+
stopConditions: [{ type: "max_file_count", value: 100, description: "Safety limit" }],
|
|
996
|
+
});
|
|
997
|
+
const result = {
|
|
998
|
+
taskId: "task-1",
|
|
999
|
+
status: "completed",
|
|
1000
|
+
modifiedPaths: ["packages/OUTSIDE/src/hack.ts"],
|
|
1001
|
+
verifications: [],
|
|
1002
|
+
risks: [],
|
|
1003
|
+
handoffNotes: [],
|
|
1004
|
+
completedAt: Date.now(),
|
|
1005
|
+
};
|
|
1006
|
+
const plan = facade.plan.submitResult(0, result);
|
|
1007
|
+
// Step should be failed, not completed
|
|
1008
|
+
(0, vitest_1.expect)(plan.steps[0].status).toBe("failed");
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
//# sourceMappingURL=session-facade.test.js.map
|