@nuvin/session 0.1.1-rc.2 → 0.2.0-rc.10
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/{chunk-WB4FEWBI.js → chunk-3CVYVANJ.js} +1 -1
- package/dist/chunk-7LEIACBZ.js +85 -0
- package/dist/{chunk-COVRYM64.js → chunk-KQHHBUVY.js} +137 -47
- package/dist/client/daemon-client.d.ts.map +1 -1
- package/dist/client/data-client.d.ts +10 -15
- package/dist/client/data-client.d.ts.map +1 -1
- package/dist/client/directory.d.ts +12 -14
- package/dist/client/directory.d.ts.map +1 -1
- package/dist/client/http-data-client.d.ts +5 -0
- package/dist/client/http-data-client.d.ts.map +1 -1
- package/dist/client/index.js +299 -40
- package/dist/client/session-client.d.ts +30 -4
- package/dist/client/session-client.d.ts.map +1 -1
- package/dist/client/websocket.d.ts.map +1 -1
- package/dist/controller/agent-channel.d.ts +10 -5
- package/dist/controller/agent-channel.d.ts.map +1 -1
- package/dist/controller/index.js +323 -48
- package/dist/controller/session-controller.d.ts +15 -14
- package/dist/controller/session-controller.d.ts.map +1 -1
- package/dist/controller/test-utils.d.ts +3 -1
- package/dist/controller/test-utils.d.ts.map +1 -1
- package/dist/protocol/data-rpc.d.ts +13 -14
- package/dist/protocol/data-rpc.d.ts.map +1 -1
- package/dist/protocol/history-text.d.ts +4 -0
- package/dist/protocol/history-text.d.ts.map +1 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +12 -4
- package/dist/protocol/model-identity.d.ts +7 -0
- package/dist/protocol/model-identity.d.ts.map +1 -1
- package/dist/protocol/types.d.ts +339 -35
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/state/approvals.d.ts +4 -15
- package/dist/state/approvals.d.ts.map +1 -1
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +15 -9
- package/dist/state/messages.d.ts +12 -2
- package/dist/state/messages.d.ts.map +1 -1
- package/dist/state/questions.d.ts +5 -0
- package/dist/state/questions.d.ts.map +1 -0
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/tool-key.d.ts +2 -0
- package/dist/state/tool-key.d.ts.map +1 -0
- package/dist/state/tool-preview.d.ts +5 -0
- package/dist/state/tool-preview.d.ts.map +1 -1
- package/dist/state/tool-preview.js +81 -29
- package/dist/test-utils/index.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-7HGJO7ZX.js +0 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-channel.d.ts","sourceRoot":"","sources":["../../src/controller/agent-channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"agent-channel.d.ts","sourceRoot":"","sources":["../../src/controller/agent-channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,OAAO,EACP,mBAAmB,EACnB,2BAA2B,EAC3B,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,kCAAkC,EAClC,4BAA4B,EAC5B,yBAAyB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,OAAO,EAAE,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,6BAA6B,CAAC;IAC3C,gFAAgF;IAChF,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CACtB,OAAO,EAAE,4BAA4B,KAClC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IACjD,QAAQ,EAAE,YAAY,CAAC;IACvB,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE3E,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,eAAe,KACrB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,CAAC,EAAE,eAAe,KACpB,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAAC;AAO9C;;;;;;;;;GASG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,eAAe,CAAoC;IAI3D,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI;IAI9D,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAIxD,mBAAmB,CACjB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B;IAOrE,mBAAmB,CACjB,OAAO,EAAE,sBAAsB,EAC/B,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,cAAc,CAAC;IAS1B,OAAO,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAOjD,eAAe,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI;IAO5D,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI;IAIrD,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI;CAG9D"}
|
package/dist/controller/index.js
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
asJsonObject,
|
|
6
6
|
createMessageStateFromMessages,
|
|
7
7
|
createSessionViewState,
|
|
8
|
-
isAutoApprovedTool,
|
|
9
8
|
isToolAutoApprovedForMode,
|
|
10
|
-
reduceSessionEvent
|
|
11
|
-
|
|
9
|
+
reduceSessionEvent,
|
|
10
|
+
scopedToolKey
|
|
11
|
+
} from "../chunk-KQHHBUVY.js";
|
|
12
12
|
|
|
13
13
|
// src/controller/agent-channel.ts
|
|
14
14
|
import { EventEmitter } from "events";
|
|
@@ -33,11 +33,11 @@ var AgentChannel = class {
|
|
|
33
33
|
}
|
|
34
34
|
return this.decider(request);
|
|
35
35
|
}
|
|
36
|
-
requestUserQuestion(request) {
|
|
36
|
+
requestUserQuestion(request, scope) {
|
|
37
37
|
if (!this.questionHandler) {
|
|
38
38
|
return Promise.reject(new Error("UI is not ready to handle user questions."));
|
|
39
39
|
}
|
|
40
|
-
return Promise.resolve(this.questionHandler(request));
|
|
40
|
+
return Promise.resolve(this.questionHandler(request, scope));
|
|
41
41
|
}
|
|
42
42
|
// ---------- UI-side (subscribers) ----------
|
|
43
43
|
onEvent(listener) {
|
|
@@ -64,10 +64,20 @@ var AgentChannel = class {
|
|
|
64
64
|
import {
|
|
65
65
|
isAbortError
|
|
66
66
|
} from "@nuvin/agent-core/shared";
|
|
67
|
+
function formatBashPolicyDenial(diagnostics) {
|
|
68
|
+
const denied = diagnostics.operations.filter((operation) => operation.decision === "deny");
|
|
69
|
+
if (denied.length === 0) {
|
|
70
|
+
return "Bash command rejected by policy.";
|
|
71
|
+
}
|
|
72
|
+
const details = denied.map((operation) => {
|
|
73
|
+
const rules = operation.matchedRuleIds.length > 0 ? `rule ${operation.matchedRuleIds.join(", ")}` : "an unmatched Deny";
|
|
74
|
+
return `${rules} at source bytes ${operation.start}-${operation.end}`;
|
|
75
|
+
});
|
|
76
|
+
return `Bash command rejected by policy: ${details.join("; ")}.`;
|
|
77
|
+
}
|
|
67
78
|
var MAX_AUTO_COMPACTION_CHAIN = 10;
|
|
68
79
|
function createSessionController(deps) {
|
|
69
80
|
const now = deps.now ?? Date.now;
|
|
70
|
-
const isAutoApproved = deps.isAutoApproved ?? isAutoApprovedTool;
|
|
71
81
|
const errorDetail = deps.errorDetail ?? ((error) => error instanceof Error ? error.message : String(error));
|
|
72
82
|
let serverCommands = deps.serverCommands ?? [];
|
|
73
83
|
let compaction = deps.compaction;
|
|
@@ -75,14 +85,20 @@ function createSessionController(deps) {
|
|
|
75
85
|
let autoCompactionChain = 0;
|
|
76
86
|
let state = createSessionViewState();
|
|
77
87
|
let seq = 0;
|
|
78
|
-
let approvalMode = deps.approvalMode ?? "
|
|
88
|
+
let approvalMode = deps.approvalMode ?? "readOnly";
|
|
79
89
|
let abortController = null;
|
|
90
|
+
let activeTurnPromise = null;
|
|
91
|
+
let closePromise = null;
|
|
92
|
+
let closing = false;
|
|
93
|
+
let detached = false;
|
|
80
94
|
let activeTurnId;
|
|
81
95
|
let activePolicyDenial;
|
|
82
96
|
let hostBusyCount = 0;
|
|
83
97
|
let questionCounter = 0;
|
|
84
98
|
const listeners = /* @__PURE__ */ new Set();
|
|
85
99
|
const approvalResolvers = /* @__PURE__ */ new Map();
|
|
100
|
+
const privateApprovals = /* @__PURE__ */ new Map();
|
|
101
|
+
const persistenceInFlight = /* @__PURE__ */ new Set();
|
|
86
102
|
const questionResolvers = /* @__PURE__ */ new Map();
|
|
87
103
|
const alwaysApproved = /* @__PURE__ */ new Set();
|
|
88
104
|
const queue = [];
|
|
@@ -133,7 +149,11 @@ function createSessionController(deps) {
|
|
|
133
149
|
emit({ type: "agent-event", event, ...scope ? { scope } : {} });
|
|
134
150
|
});
|
|
135
151
|
const unsubscribeLoaded = deps.channel.onSessionLoaded((loaded) => {
|
|
152
|
+
if (persistenceInFlight.size > 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
136
155
|
queue.length = 0;
|
|
156
|
+
alwaysApproved.clear();
|
|
137
157
|
abortController?.abort(new Error("Session loaded; the active turn was cancelled."));
|
|
138
158
|
rejectAllPending("Session loaded; pending interactions were cancelled.");
|
|
139
159
|
emit({
|
|
@@ -151,10 +171,12 @@ function createSessionController(deps) {
|
|
|
151
171
|
});
|
|
152
172
|
});
|
|
153
173
|
deps.channel.setToolDecider((request) => {
|
|
154
|
-
const { toolCall, agentId, parentToolCallId, dirAccess } = request;
|
|
174
|
+
const { toolCall, agentId, parentToolCallId, dirAccess, bashPolicy } = request;
|
|
155
175
|
const scope = parentToolCallId !== void 0 ? { agentId, parentToolCallId } : void 0;
|
|
156
|
-
const approvalKey = `${agentId}:${toolCall.name}`;
|
|
157
|
-
const
|
|
176
|
+
const approvalKey = request.approvalKey ?? `${agentId}:${toolCall.name}`;
|
|
177
|
+
const permissionClass = request.permissionClass ?? "always-confirm";
|
|
178
|
+
const hasBashPolicy = toolCall.name === "Bash" && bashPolicy !== void 0;
|
|
179
|
+
const auto = !dirAccess && (alwaysApproved.has(approvalKey) || isToolAutoApprovedForMode(approvalMode, permissionClass));
|
|
158
180
|
if (dirAccess && approvalMode === "sudo") {
|
|
159
181
|
emit({
|
|
160
182
|
type: "agent-event",
|
|
@@ -162,9 +184,50 @@ function createSessionController(deps) {
|
|
|
162
184
|
toolStatus: "approved",
|
|
163
185
|
...scope ? { scope } : {}
|
|
164
186
|
});
|
|
165
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
action: "run",
|
|
189
|
+
grantDir: { dir: dirAccess.proposedDir, persist: false },
|
|
190
|
+
...toolCall.name === "Bash" ? { approvalSource: "sudo" } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (hasBashPolicy && approvalMode === "sudo") {
|
|
194
|
+
emit({
|
|
195
|
+
type: "agent-event",
|
|
196
|
+
event: { type: "tool_call", toolCall },
|
|
197
|
+
toolStatus: "approved",
|
|
198
|
+
...scope ? { scope } : {}
|
|
199
|
+
});
|
|
200
|
+
return { action: "run", approvalSource: "sudo" };
|
|
201
|
+
}
|
|
202
|
+
if (hasBashPolicy && bashPolicy.decision === "deny") {
|
|
203
|
+
emit({
|
|
204
|
+
type: "agent-event",
|
|
205
|
+
event: { type: "tool_call", toolCall },
|
|
206
|
+
toolStatus: "rejected",
|
|
207
|
+
...scope ? { scope } : {}
|
|
208
|
+
});
|
|
209
|
+
activePolicyDenial = formatBashPolicyDenial(bashPolicy);
|
|
210
|
+
return { action: "reject", reason: activePolicyDenial };
|
|
211
|
+
}
|
|
212
|
+
if (!dirAccess && hasBashPolicy && bashPolicy.decision === "allow") {
|
|
213
|
+
emit({
|
|
214
|
+
type: "agent-event",
|
|
215
|
+
event: { type: "tool_call", toolCall },
|
|
216
|
+
toolStatus: "approved",
|
|
217
|
+
...scope ? { scope } : {}
|
|
218
|
+
});
|
|
219
|
+
return { action: "run", approvalSource: "policy" };
|
|
220
|
+
}
|
|
221
|
+
if (!dirAccess && hasBashPolicy && alwaysApproved.has(approvalKey)) {
|
|
222
|
+
emit({
|
|
223
|
+
type: "agent-event",
|
|
224
|
+
event: { type: "tool_call", toolCall },
|
|
225
|
+
toolStatus: "approved",
|
|
226
|
+
...scope ? { scope } : {}
|
|
227
|
+
});
|
|
228
|
+
return { action: "run", approvalSource: "remembered" };
|
|
166
229
|
}
|
|
167
|
-
if (auto) {
|
|
230
|
+
if (auto && !hasBashPolicy) {
|
|
168
231
|
emit({
|
|
169
232
|
type: "agent-event",
|
|
170
233
|
event: { type: "tool_call", toolCall },
|
|
@@ -187,20 +250,33 @@ function createSessionController(deps) {
|
|
|
187
250
|
const input = asJsonObject(toolCall.input);
|
|
188
251
|
const descriptor = {
|
|
189
252
|
agentId,
|
|
190
|
-
|
|
253
|
+
permissionClass,
|
|
254
|
+
summary: findToolSummary(toolCall.id, parentToolCallId),
|
|
191
255
|
toolCallId: toolCall.id,
|
|
192
256
|
toolName: toolCall.name,
|
|
193
257
|
...dirAccess !== void 0 ? { dirAccess } : {},
|
|
194
258
|
...input !== void 0 ? { input } : {},
|
|
195
259
|
...parentToolCallId !== void 0 ? { parentToolCallId } : {},
|
|
196
|
-
...nickname !== void 0 ? { nickname } : {}
|
|
260
|
+
...nickname !== void 0 ? { nickname } : {},
|
|
261
|
+
...!dirAccess && bashPolicy !== void 0 ? { bashPolicy } : {},
|
|
262
|
+
...request.workflowBashPolicy !== void 0 ? { workflowBashPolicy: request.workflowBashPolicy } : {}
|
|
197
263
|
};
|
|
198
264
|
return new Promise((resolve) => {
|
|
199
|
-
|
|
265
|
+
const toolKey = scopedToolKey(parentToolCallId, toolCall.id);
|
|
266
|
+
approvalResolvers.set(toolKey, {
|
|
267
|
+
resolve,
|
|
268
|
+
toolCallId: toolCall.id,
|
|
269
|
+
...parentToolCallId !== void 0 ? { parentToolCallId } : {}
|
|
270
|
+
});
|
|
271
|
+
privateApprovals.set(toolKey, {
|
|
272
|
+
approvalKey,
|
|
273
|
+
bashPolicy: hasBashPolicy,
|
|
274
|
+
...!dirAccess && toolCall.name === "Bash" && bashPolicy?.persistence.eligible && request.persistBashPermission !== void 0 ? { persistBashPermission: request.persistBashPermission } : {}
|
|
275
|
+
});
|
|
200
276
|
emit({ type: "approval-requested", approval: descriptor });
|
|
201
277
|
});
|
|
202
278
|
});
|
|
203
|
-
deps.channel.setQuestionHandler((request) => {
|
|
279
|
+
deps.channel.setQuestionHandler((request, scope) => {
|
|
204
280
|
if (deps.headless) {
|
|
205
281
|
activePolicyDenial = "Questions are unavailable in headless mode.";
|
|
206
282
|
return Promise.reject(new Error(activePolicyDenial));
|
|
@@ -209,41 +285,71 @@ function createSessionController(deps) {
|
|
|
209
285
|
const questionId = `question-${questionCounter}`;
|
|
210
286
|
return new Promise((resolve, reject) => {
|
|
211
287
|
questionResolvers.set(questionId, { resolve, reject });
|
|
212
|
-
|
|
288
|
+
const nickname = nicknameFor(scope?.parentToolCallId);
|
|
289
|
+
emit({
|
|
290
|
+
type: "question-asked",
|
|
291
|
+
question: {
|
|
292
|
+
questionId,
|
|
293
|
+
request,
|
|
294
|
+
...scope?.agentId !== void 0 ? { agentId: scope.agentId } : {},
|
|
295
|
+
...scope?.parentToolCallId !== void 0 ? { parentToolCallId: scope.parentToolCallId } : {},
|
|
296
|
+
...nickname !== void 0 ? { nickname } : {}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
213
299
|
});
|
|
214
300
|
});
|
|
215
|
-
function findToolSummary(toolCallId) {
|
|
216
|
-
const messageId = state.messages.toolMessageIds[toolCallId];
|
|
301
|
+
function findToolSummary(toolCallId, parentToolCallId) {
|
|
302
|
+
const messageId = state.messages.toolMessageIds[scopedToolKey(parentToolCallId, toolCallId)];
|
|
217
303
|
const message = state.messages.messages.find((entry) => entry.id === messageId);
|
|
218
304
|
return message?.role === "tool" ? message.summary : "";
|
|
219
305
|
}
|
|
220
306
|
function nicknameFor(parentToolCallId) {
|
|
221
307
|
if (!parentToolCallId) return void 0;
|
|
222
|
-
const parentMessageId = state.messages.toolMessageIds[parentToolCallId];
|
|
308
|
+
const parentMessageId = state.messages.toolMessageIds[scopedToolKey(void 0, parentToolCallId)];
|
|
223
309
|
const parentMessage = state.messages.messages.find((entry) => entry.id === parentMessageId);
|
|
224
310
|
if (parentMessage?.role !== "tool") return void 0;
|
|
225
311
|
const nickname = parentMessage.input?.nickname;
|
|
226
312
|
return typeof nickname === "string" ? nickname : void 0;
|
|
227
313
|
}
|
|
228
|
-
function findDescriptor(toolCallId) {
|
|
314
|
+
function findDescriptor(toolCallId, parentToolCallId) {
|
|
229
315
|
return [state.approval.active, ...state.approval.pending].find(
|
|
230
|
-
(entry) => entry?.toolCallId === toolCallId
|
|
316
|
+
(entry) => entry?.toolCallId === toolCallId && entry.parentToolCallId === parentToolCallId
|
|
231
317
|
);
|
|
232
318
|
}
|
|
233
319
|
function rejectAllPending(reason) {
|
|
234
|
-
for (const
|
|
235
|
-
resolve({ action: "reject", reason });
|
|
236
|
-
emit({
|
|
320
|
+
for (const pending of approvalResolvers.values()) {
|
|
321
|
+
pending.resolve({ action: "reject", reason });
|
|
322
|
+
emit({
|
|
323
|
+
type: "approval-settled",
|
|
324
|
+
toolCallId: pending.toolCallId,
|
|
325
|
+
...pending.parentToolCallId === void 0 ? {} : { parentToolCallId: pending.parentToolCallId },
|
|
326
|
+
status: "rejected",
|
|
327
|
+
by: "abort"
|
|
328
|
+
});
|
|
237
329
|
}
|
|
238
330
|
approvalResolvers.clear();
|
|
331
|
+
privateApprovals.clear();
|
|
332
|
+
persistenceInFlight.clear();
|
|
239
333
|
for (const [questionId, { reject }] of questionResolvers) {
|
|
240
334
|
reject(new Error(reason));
|
|
241
335
|
emit({ type: "question-settled", questionId });
|
|
242
336
|
}
|
|
243
337
|
questionResolvers.clear();
|
|
244
338
|
}
|
|
339
|
+
async function guardQueuedSteering(input) {
|
|
340
|
+
if (!guardPrompt) return { ok: true, prompt: input };
|
|
341
|
+
try {
|
|
342
|
+
const guarded = await guardPrompt(input);
|
|
343
|
+
if ("blocked" in guarded) {
|
|
344
|
+
return { ok: false, message: `Prompt blocked: ${guarded.blocked.reason}` };
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, prompt: guarded.prompt };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return { ok: false, message: errorDetail(error) };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
245
351
|
function startNextQueuedSubmission() {
|
|
246
|
-
if (abortController || hostBusyCount > 0) return;
|
|
352
|
+
if (closing || abortController || hostBusyCount > 0) return;
|
|
247
353
|
const next = queue.shift();
|
|
248
354
|
if (!next) return;
|
|
249
355
|
if (!next.opts.silent) {
|
|
@@ -256,7 +362,12 @@ function createSessionController(deps) {
|
|
|
256
362
|
next.opts.turnId
|
|
257
363
|
);
|
|
258
364
|
}
|
|
259
|
-
void
|
|
365
|
+
void startTrackedTurn(next.input, {
|
|
366
|
+
turnId: next.opts.turnId,
|
|
367
|
+
allowSteering: next.opts.turnId === void 0,
|
|
368
|
+
submission: next.opts.submission ?? { origin: "user" },
|
|
369
|
+
...next.opts.signal === void 0 ? {} : { signal: next.opts.signal }
|
|
370
|
+
});
|
|
260
371
|
}
|
|
261
372
|
function finalizeTurn(controller, reason) {
|
|
262
373
|
if (abortController !== controller) return false;
|
|
@@ -267,6 +378,39 @@ function createSessionController(deps) {
|
|
|
267
378
|
}
|
|
268
379
|
async function runTurn(input, opts) {
|
|
269
380
|
const controller = new AbortController();
|
|
381
|
+
const abortFromCaller = () => controller.abort(opts.signal?.reason);
|
|
382
|
+
if (opts.signal !== void 0) {
|
|
383
|
+
if (opts.signal.aborted) abortFromCaller();
|
|
384
|
+
else opts.signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
385
|
+
}
|
|
386
|
+
const drainSteering = async () => {
|
|
387
|
+
if (abortController !== controller || controller.signal.aborted) return [];
|
|
388
|
+
const barrierIndex = queue.findIndex((entry) => entry.delivery === "next-turn");
|
|
389
|
+
const steeringCount = barrierIndex === -1 ? queue.length : barrierIndex;
|
|
390
|
+
if (steeringCount === 0) return [];
|
|
391
|
+
const drained = queue.splice(0, steeringCount);
|
|
392
|
+
emitTurnStatus();
|
|
393
|
+
const accepted = [];
|
|
394
|
+
for (const entry of drained) {
|
|
395
|
+
const guarded = await guardQueuedSteering(entry.input);
|
|
396
|
+
if (guarded.ok) {
|
|
397
|
+
accepted.push({ input: guarded.prompt, opts: entry.opts });
|
|
398
|
+
} else {
|
|
399
|
+
emit({ type: "error", message: guarded.message });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (abortController !== controller || controller.signal.aborted) return [];
|
|
403
|
+
for (const entry of accepted) {
|
|
404
|
+
if (!entry.opts.silent) {
|
|
405
|
+
emit({
|
|
406
|
+
type: "user-message",
|
|
407
|
+
text: entry.opts.displayText,
|
|
408
|
+
...entry.opts.attachmentLabels?.length ? { attachmentLabels: entry.opts.attachmentLabels } : {}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return accepted.map((entry) => entry.input);
|
|
413
|
+
};
|
|
270
414
|
const previousTurnId = activeTurnId;
|
|
271
415
|
if (opts.turnId !== void 0) activeTurnId = opts.turnId;
|
|
272
416
|
if (!opts.systemInitiated) activePolicyDenial = void 0;
|
|
@@ -296,7 +440,12 @@ function createSessionController(deps) {
|
|
|
296
440
|
}
|
|
297
441
|
finalInput = guarded.prompt;
|
|
298
442
|
}
|
|
299
|
-
const compactionSend = (text) => runTurn(text, {
|
|
443
|
+
const compactionSend = (text) => runTurn(text, {
|
|
444
|
+
systemInitiated: true,
|
|
445
|
+
allowSteering: opts.allowSteering,
|
|
446
|
+
submission: opts.submission,
|
|
447
|
+
...opts.signal === void 0 ? {} : { signal: opts.signal }
|
|
448
|
+
});
|
|
300
449
|
const compactionIo = {
|
|
301
450
|
signal: controller.signal,
|
|
302
451
|
send: compactionSend,
|
|
@@ -319,7 +468,9 @@ function createSessionController(deps) {
|
|
|
319
468
|
try {
|
|
320
469
|
const result = await deps.agent.send(finalInput, {
|
|
321
470
|
streaming: true,
|
|
322
|
-
signal: controller.signal
|
|
471
|
+
signal: controller.signal,
|
|
472
|
+
submission: opts.submission,
|
|
473
|
+
...opts.allowSteering ? { drainSteering } : {}
|
|
323
474
|
});
|
|
324
475
|
if (typeof result === "object" && result !== null && "status" in result && result.status === "aborted") {
|
|
325
476
|
terminalStatus = "aborted";
|
|
@@ -350,6 +501,7 @@ function createSessionController(deps) {
|
|
|
350
501
|
}
|
|
351
502
|
}
|
|
352
503
|
} finally {
|
|
504
|
+
opts.signal?.removeEventListener("abort", abortFromCaller);
|
|
353
505
|
const ownedTurn = finalizeTurn(
|
|
354
506
|
controller,
|
|
355
507
|
"Turn ended before pending interactions could be resolved."
|
|
@@ -374,10 +526,28 @@ function createSessionController(deps) {
|
|
|
374
526
|
if (ownedTurn) startNextQueuedSubmission();
|
|
375
527
|
}
|
|
376
528
|
}
|
|
529
|
+
function startTrackedTurn(input, opts) {
|
|
530
|
+
let tracked;
|
|
531
|
+
tracked = runTurn(input, opts).finally(() => {
|
|
532
|
+
if (activeTurnPromise === tracked) activeTurnPromise = null;
|
|
533
|
+
});
|
|
534
|
+
activeTurnPromise = tracked;
|
|
535
|
+
return tracked;
|
|
536
|
+
}
|
|
537
|
+
function detachController() {
|
|
538
|
+
if (detached) return;
|
|
539
|
+
detached = true;
|
|
540
|
+
unsubscribeEvents();
|
|
541
|
+
unsubscribeLoaded();
|
|
542
|
+
deps.channel.setToolDecider(null);
|
|
543
|
+
deps.channel.setQuestionHandler(null);
|
|
544
|
+
listeners.clear();
|
|
545
|
+
}
|
|
377
546
|
return {
|
|
378
547
|
abort() {
|
|
379
548
|
queue.length = 0;
|
|
380
549
|
abortController?.abort(new Error("User aborted the current turn."));
|
|
550
|
+
rejectAllPending("User aborted the current turn.");
|
|
381
551
|
},
|
|
382
552
|
attachHostHooks(hooks) {
|
|
383
553
|
if (hooks.compaction) compaction = hooks.compaction;
|
|
@@ -398,13 +568,21 @@ function createSessionController(deps) {
|
|
|
398
568
|
emit({ type: "info", message });
|
|
399
569
|
},
|
|
400
570
|
close() {
|
|
571
|
+
if (closePromise) return closePromise;
|
|
572
|
+
closing = true;
|
|
573
|
+
const shouldEmitTurnStatus = queue.length > 0 || abortController !== null || hostBusyCount > 0;
|
|
574
|
+
queue.length = 0;
|
|
401
575
|
abortController?.abort(new Error("Session closed."));
|
|
402
576
|
rejectAllPending("Session closed.");
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
577
|
+
if (shouldEmitTurnStatus) emitTurnStatus();
|
|
578
|
+
const active = activeTurnPromise;
|
|
579
|
+
if (!active) {
|
|
580
|
+
detachController();
|
|
581
|
+
closePromise = Promise.resolve();
|
|
582
|
+
return closePromise;
|
|
583
|
+
}
|
|
584
|
+
closePromise = active.then(detachController, detachController);
|
|
585
|
+
return closePromise;
|
|
408
586
|
},
|
|
409
587
|
dequeue(queuedId) {
|
|
410
588
|
const index = queue.findIndex((q) => q.id === queuedId);
|
|
@@ -413,20 +591,32 @@ function createSessionController(deps) {
|
|
|
413
591
|
emitTurnStatus();
|
|
414
592
|
return true;
|
|
415
593
|
},
|
|
416
|
-
decideApproval(toolCallId, decision, comment, grantDir) {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
|
|
594
|
+
decideApproval(toolCallId, decision, comment, grantDir, parentToolCallId) {
|
|
595
|
+
const toolKey = scopedToolKey(parentToolCallId, toolCallId);
|
|
596
|
+
if (persistenceInFlight.has(toolKey)) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const pending = approvalResolvers.get(toolKey);
|
|
600
|
+
if (!pending) return false;
|
|
601
|
+
approvalResolvers.delete(toolKey);
|
|
602
|
+
persistenceInFlight.delete(toolKey);
|
|
603
|
+
const descriptor = findDescriptor(toolCallId, parentToolCallId);
|
|
604
|
+
const privateApproval = privateApprovals.get(toolKey);
|
|
605
|
+
privateApprovals.delete(toolKey);
|
|
421
606
|
if (decision === "a" && descriptor && !descriptor.dirAccess) {
|
|
422
|
-
alwaysApproved.add(
|
|
607
|
+
alwaysApproved.add(
|
|
608
|
+
privateApproval?.approvalKey ?? `${descriptor.agentId}:${descriptor.toolName}`
|
|
609
|
+
);
|
|
423
610
|
}
|
|
424
611
|
const rejected = decision === "n";
|
|
425
612
|
if (rejected) {
|
|
426
613
|
const baseReason = `User rejected tool execution (${descriptor?.toolName ?? "unknown tool"})`;
|
|
427
|
-
resolve({
|
|
614
|
+
pending.resolve({
|
|
615
|
+
action: "reject",
|
|
616
|
+
reason: comment ? `${baseReason}: ${comment}` : baseReason
|
|
617
|
+
});
|
|
428
618
|
} else if (descriptor?.dirAccess) {
|
|
429
|
-
resolve({
|
|
619
|
+
pending.resolve({
|
|
430
620
|
action: "run",
|
|
431
621
|
grantDir: {
|
|
432
622
|
dir: resolveGrantDir(
|
|
@@ -435,19 +625,85 @@ function createSessionController(deps) {
|
|
|
435
625
|
grantDir
|
|
436
626
|
),
|
|
437
627
|
persist: decision === "a"
|
|
438
|
-
}
|
|
628
|
+
},
|
|
629
|
+
...descriptor.toolName === "Bash" && privateApproval?.bashPolicy ? {
|
|
630
|
+
approvalDecision: decision,
|
|
631
|
+
approvalSource: "user"
|
|
632
|
+
} : {}
|
|
633
|
+
});
|
|
634
|
+
} else if (descriptor?.toolName === "Bash" && privateApproval?.bashPolicy) {
|
|
635
|
+
pending.resolve({
|
|
636
|
+
action: "run",
|
|
637
|
+
approvalDecision: decision,
|
|
638
|
+
approvalSource: "user"
|
|
439
639
|
});
|
|
440
640
|
} else {
|
|
441
|
-
resolve({ action: "run" });
|
|
641
|
+
pending.resolve({ action: "run" });
|
|
442
642
|
}
|
|
443
643
|
emit({
|
|
444
644
|
type: "approval-settled",
|
|
445
645
|
toolCallId,
|
|
646
|
+
...parentToolCallId !== void 0 ? { parentToolCallId } : {},
|
|
446
647
|
status: rejected ? "rejected" : "approved",
|
|
447
648
|
by: "client"
|
|
448
649
|
});
|
|
449
650
|
return true;
|
|
450
651
|
},
|
|
652
|
+
async persistBashPermission(toolCallId, request, parentToolCallId) {
|
|
653
|
+
const toolKey = scopedToolKey(parentToolCallId, toolCallId);
|
|
654
|
+
const descriptor = state.approval.active;
|
|
655
|
+
const privateApproval = privateApprovals.get(toolKey);
|
|
656
|
+
const handler = privateApproval?.persistBashPermission;
|
|
657
|
+
if (descriptor?.toolCallId !== toolCallId || descriptor.parentToolCallId !== parentToolCallId || descriptor.toolName !== "Bash" || descriptor.dirAccess !== void 0 || descriptor.bashPolicy?.persistence.eligible !== true || handler === void 0 || !approvalResolvers.has(toolKey)) {
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
if (persistenceInFlight.has(toolKey)) {
|
|
661
|
+
return {
|
|
662
|
+
status: "validation-error",
|
|
663
|
+
message: "A persistent Bash permission save is already in progress.",
|
|
664
|
+
fieldErrors: { server: "Wait for the current save to finish." }
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (hostBusyCount > 0) {
|
|
668
|
+
return {
|
|
669
|
+
status: "validation-error",
|
|
670
|
+
message: "A server operation is already in progress.",
|
|
671
|
+
fieldErrors: { server: "Wait for the current operation to finish." }
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
persistenceInFlight.add(toolKey);
|
|
675
|
+
hostBusyCount += 1;
|
|
676
|
+
emitTurnStatus();
|
|
677
|
+
try {
|
|
678
|
+
const outcome = await handler(request);
|
|
679
|
+
if (outcome.status === "validation-error") {
|
|
680
|
+
return outcome;
|
|
681
|
+
}
|
|
682
|
+
const pending = approvalResolvers.get(toolKey);
|
|
683
|
+
if (pending === void 0 || state.approval.active?.toolCallId !== toolCallId || state.approval.active.parentToolCallId !== parentToolCallId || privateApprovals.get(toolKey) !== privateApproval) {
|
|
684
|
+
return void 0;
|
|
685
|
+
}
|
|
686
|
+
approvalResolvers.delete(toolKey);
|
|
687
|
+
privateApprovals.delete(toolKey);
|
|
688
|
+
pending.resolve({
|
|
689
|
+
action: "run",
|
|
690
|
+
executionAuthorization: outcome.executionAuthorization,
|
|
691
|
+
approvalSource: "user"
|
|
692
|
+
});
|
|
693
|
+
emit({
|
|
694
|
+
type: "approval-settled",
|
|
695
|
+
toolCallId,
|
|
696
|
+
...parentToolCallId !== void 0 ? { parentToolCallId } : {},
|
|
697
|
+
status: "approved",
|
|
698
|
+
by: "client"
|
|
699
|
+
});
|
|
700
|
+
return { status: "approved" };
|
|
701
|
+
} finally {
|
|
702
|
+
persistenceInFlight.delete(toolKey);
|
|
703
|
+
hostBusyCount = Math.max(0, hostBusyCount - 1);
|
|
704
|
+
emitTurnStatus();
|
|
705
|
+
}
|
|
706
|
+
},
|
|
451
707
|
echoUserMessage(text, attachmentLabels) {
|
|
452
708
|
emit({
|
|
453
709
|
type: "user-message",
|
|
@@ -461,6 +717,9 @@ function createSessionController(deps) {
|
|
|
461
717
|
isHostBusy() {
|
|
462
718
|
return hostBusyCount > 0;
|
|
463
719
|
},
|
|
720
|
+
isTurnActive() {
|
|
721
|
+
return abortController !== null;
|
|
722
|
+
},
|
|
464
723
|
onEvent(listener) {
|
|
465
724
|
listeners.add(listener);
|
|
466
725
|
return () => listeners.delete(listener);
|
|
@@ -484,12 +743,15 @@ function createSessionController(deps) {
|
|
|
484
743
|
emit({ type: "workflow-progress", runId, delta });
|
|
485
744
|
},
|
|
486
745
|
reset() {
|
|
746
|
+
if (persistenceInFlight.size > 0) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
487
749
|
queue.length = 0;
|
|
488
750
|
hostBusyCount = 0;
|
|
489
751
|
abortController?.abort(new Error("Session reset."));
|
|
490
752
|
rejectAllPending("Session reset; pending interactions were cancelled.");
|
|
491
753
|
alwaysApproved.clear();
|
|
492
|
-
approvalMode = "
|
|
754
|
+
approvalMode = "readOnly";
|
|
493
755
|
emit({
|
|
494
756
|
type: "state-reset",
|
|
495
757
|
state: { ...createSessionViewState(), meta: state.meta, mcp: state.mcp }
|
|
@@ -508,8 +770,15 @@ function createSessionController(deps) {
|
|
|
508
770
|
startNextQueuedSubmission();
|
|
509
771
|
},
|
|
510
772
|
async submit(input, opts) {
|
|
773
|
+
if (closing) throw new Error("Session is closing.");
|
|
774
|
+
opts.signal?.throwIfAborted();
|
|
511
775
|
if (abortController || hostBusyCount > 0) {
|
|
512
|
-
queue.push({
|
|
776
|
+
queue.push({
|
|
777
|
+
id: `q-${++queueCounter}`,
|
|
778
|
+
input,
|
|
779
|
+
opts,
|
|
780
|
+
delivery: abortController !== null && opts.turnId === void 0 ? "steering" : "next-turn"
|
|
781
|
+
});
|
|
513
782
|
emitTurnStatus();
|
|
514
783
|
return;
|
|
515
784
|
}
|
|
@@ -523,11 +792,17 @@ function createSessionController(deps) {
|
|
|
523
792
|
opts.turnId
|
|
524
793
|
);
|
|
525
794
|
}
|
|
526
|
-
await
|
|
795
|
+
await startTrackedTurn(input, {
|
|
796
|
+
turnId: opts.turnId,
|
|
797
|
+
allowSteering: opts.turnId === void 0,
|
|
798
|
+
submission: opts.submission ?? { origin: "user" },
|
|
799
|
+
...opts.signal === void 0 ? {} : { signal: opts.signal }
|
|
800
|
+
});
|
|
527
801
|
}
|
|
528
802
|
};
|
|
529
803
|
}
|
|
530
804
|
export {
|
|
531
805
|
AgentChannel,
|
|
532
|
-
createSessionController
|
|
806
|
+
createSessionController,
|
|
807
|
+
formatBashPolicyDenial
|
|
533
808
|
};
|