@nuvin/session 0.1.0-rc.5

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.
Files changed (72) hide show
  1. package/dist/chunk-3IPZO7LP.js +66 -0
  2. package/dist/chunk-7G3R25NS.js +6 -0
  3. package/dist/chunk-DGC7JSCG.js +0 -0
  4. package/dist/chunk-FR3R66RD.js +678 -0
  5. package/dist/chunk-UVJIG4DT.js +17 -0
  6. package/dist/client/directory.d.ts +141 -0
  7. package/dist/client/directory.d.ts.map +1 -0
  8. package/dist/client/endpoint.d.ts +33 -0
  9. package/dist/client/endpoint.d.ts.map +1 -0
  10. package/dist/client/index.d.ts +8 -0
  11. package/dist/client/index.d.ts.map +1 -0
  12. package/dist/client/index.js +1060 -0
  13. package/dist/client/session-client.d.ts +60 -0
  14. package/dist/client/session-client.d.ts.map +1 -0
  15. package/dist/client/socket.d.ts +38 -0
  16. package/dist/client/socket.d.ts.map +1 -0
  17. package/dist/client/transport.d.ts +9 -0
  18. package/dist/client/transport.d.ts.map +1 -0
  19. package/dist/client/uds-socket.d.ts +23 -0
  20. package/dist/client/uds-socket.d.ts.map +1 -0
  21. package/dist/client/websocket.d.ts +54 -0
  22. package/dist/client/websocket.d.ts.map +1 -0
  23. package/dist/controller/agent-channel.d.ts +58 -0
  24. package/dist/controller/agent-channel.d.ts.map +1 -0
  25. package/dist/controller/index.d.ts +3 -0
  26. package/dist/controller/index.d.ts.map +1 -0
  27. package/dist/controller/index.js +441 -0
  28. package/dist/controller/session-controller.d.ts +147 -0
  29. package/dist/controller/session-controller.d.ts.map +1 -0
  30. package/dist/controller/test-utils.d.ts +15 -0
  31. package/dist/controller/test-utils.d.ts.map +1 -0
  32. package/dist/grant/index.d.ts +33 -0
  33. package/dist/grant/index.d.ts.map +1 -0
  34. package/dist/grant/index.js +12 -0
  35. package/dist/protocol/index.d.ts +3 -0
  36. package/dist/protocol/index.d.ts.map +1 -0
  37. package/dist/protocol/index.js +7 -0
  38. package/dist/protocol/types.d.ts +855 -0
  39. package/dist/protocol/types.d.ts.map +1 -0
  40. package/dist/protocol/version.d.ts +8 -0
  41. package/dist/protocol/version.d.ts.map +1 -0
  42. package/dist/state/approvals.d.ts +34 -0
  43. package/dist/state/approvals.d.ts.map +1 -0
  44. package/dist/state/dir-access.d.ts +9 -0
  45. package/dist/state/dir-access.d.ts.map +1 -0
  46. package/dist/state/index.d.ts +6 -0
  47. package/dist/state/index.d.ts.map +1 -0
  48. package/dist/state/index.js +48 -0
  49. package/dist/state/json.d.ts +7 -0
  50. package/dist/state/json.d.ts.map +1 -0
  51. package/dist/state/messages.d.ts +105 -0
  52. package/dist/state/messages.d.ts.map +1 -0
  53. package/dist/state/session.d.ts +9 -0
  54. package/dist/state/session.d.ts.map +1 -0
  55. package/dist/state/tool-preview.d.ts +31 -0
  56. package/dist/state/tool-preview.d.ts.map +1 -0
  57. package/dist/state/tool-preview.js +300 -0
  58. package/dist/state/workflow-view.d.ts +10 -0
  59. package/dist/state/workflow-view.d.ts.map +1 -0
  60. package/dist/test-utils/fake-relay.d.ts +24 -0
  61. package/dist/test-utils/fake-relay.d.ts.map +1 -0
  62. package/dist/test-utils/index.d.ts +2 -0
  63. package/dist/test-utils/index.d.ts.map +1 -0
  64. package/dist/test-utils/index.js +250 -0
  65. package/dist/ui/history-grouping.d.ts +36 -0
  66. package/dist/ui/history-grouping.d.ts.map +1 -0
  67. package/dist/ui/index.d.ts +3 -0
  68. package/dist/ui/index.d.ts.map +1 -0
  69. package/dist/ui/index.js +135 -0
  70. package/dist/ui/picker-rows.d.ts +62 -0
  71. package/dist/ui/picker-rows.d.ts.map +1 -0
  72. package/package.json +70 -0
@@ -0,0 +1,441 @@
1
+ import {
2
+ resolveGrantDir
3
+ } from "../chunk-UVJIG4DT.js";
4
+ import {
5
+ asJsonObject,
6
+ createMessageStateFromMessages,
7
+ createSessionViewState,
8
+ isAutoApprovedTool,
9
+ reduceSessionEvent
10
+ } from "../chunk-FR3R66RD.js";
11
+
12
+ // src/controller/agent-channel.ts
13
+ import { EventEmitter } from "events";
14
+ var NO_DECIDER_DECISION = {
15
+ action: "reject",
16
+ reason: "UI is not ready to handle tool calls."
17
+ };
18
+ var AgentChannel = class {
19
+ emitter = new EventEmitter();
20
+ decider = null;
21
+ questionHandler = null;
22
+ // ---------- Agent-side (publishers) ----------
23
+ publishEvent(event, scope) {
24
+ this.emitter.emit("event", { event, scope });
25
+ }
26
+ publishSessionLoaded(loaded) {
27
+ this.emitter.emit("session-loaded", loaded);
28
+ }
29
+ requestToolDecision(request) {
30
+ if (!this.decider) {
31
+ return NO_DECIDER_DECISION;
32
+ }
33
+ return this.decider(request);
34
+ }
35
+ requestUserQuestion(request) {
36
+ if (!this.questionHandler) {
37
+ return Promise.reject(new Error("UI is not ready to handle user questions."));
38
+ }
39
+ return Promise.resolve(this.questionHandler(request));
40
+ }
41
+ // ---------- UI-side (subscribers) ----------
42
+ onEvent(listener) {
43
+ this.emitter.on("event", listener);
44
+ return () => {
45
+ this.emitter.off("event", listener);
46
+ };
47
+ }
48
+ onSessionLoaded(listener) {
49
+ this.emitter.on("session-loaded", listener);
50
+ return () => {
51
+ this.emitter.off("session-loaded", listener);
52
+ };
53
+ }
54
+ setToolDecider(decider) {
55
+ this.decider = decider;
56
+ }
57
+ setQuestionHandler(handler) {
58
+ this.questionHandler = handler;
59
+ }
60
+ };
61
+
62
+ // src/controller/session-controller.ts
63
+ import {
64
+ isAbortError
65
+ } from "@nuvin/agent-core/shared";
66
+ var MAX_AUTO_COMPACTION_CHAIN = 10;
67
+ function createSessionController(deps) {
68
+ const now = deps.now ?? Date.now;
69
+ const isAutoApproved = deps.isAutoApproved ?? isAutoApprovedTool;
70
+ const errorDetail = deps.errorDetail ?? ((error) => String(error));
71
+ const serverCommands = deps.serverCommands ?? [];
72
+ let compaction = deps.compaction;
73
+ let guardPrompt = deps.guardPrompt;
74
+ let autoCompactionChain = 0;
75
+ let state = createSessionViewState();
76
+ let seq = 0;
77
+ let sudoMode = false;
78
+ let abortController = null;
79
+ let questionCounter = 0;
80
+ const listeners = /* @__PURE__ */ new Set();
81
+ const approvalResolvers = /* @__PURE__ */ new Map();
82
+ const questionResolvers = /* @__PURE__ */ new Map();
83
+ const alwaysApproved = /* @__PURE__ */ new Set();
84
+ const queue = [];
85
+ let queueCounter = 0;
86
+ const deliveryQueue = [];
87
+ let draining = false;
88
+ const emit = (body) => {
89
+ seq += 1;
90
+ const event = Object.assign({}, body, { seq, at: now() });
91
+ state = reduceSessionEvent(state, event);
92
+ deliveryQueue.push(event);
93
+ if (draining) return;
94
+ draining = true;
95
+ try {
96
+ while (deliveryQueue.length > 0) {
97
+ const next = deliveryQueue.shift();
98
+ for (const listener of listeners) {
99
+ try {
100
+ listener(next);
101
+ } catch {
102
+ }
103
+ }
104
+ }
105
+ } finally {
106
+ draining = false;
107
+ }
108
+ };
109
+ const emitTurnStatus = (busy) => {
110
+ emit({
111
+ type: "turn-status",
112
+ busy,
113
+ queuedCount: queue.length,
114
+ queued: queue.map((q) => ({
115
+ id: q.id,
116
+ displayText: q.opts.displayText,
117
+ ...q.opts.attachmentLabels?.length ? { attachmentLabels: q.opts.attachmentLabels } : {}
118
+ }))
119
+ });
120
+ };
121
+ const unsubscribeEvents = deps.channel.onEvent(({ event, scope }) => {
122
+ if (event.type === "tool_call") return;
123
+ emit({ type: "agent-event", event, ...scope ? { scope } : {} });
124
+ });
125
+ const unsubscribeLoaded = deps.channel.onSessionLoaded((loaded) => {
126
+ abortController?.abort(new Error("Session loaded; the active turn was cancelled."));
127
+ rejectAllPending("Session loaded; pending interactions were cancelled.");
128
+ emit({
129
+ type: "state-reset",
130
+ // Loading a transcript replaces the live interaction state (messages,
131
+ // approvals, questions) but NOT the session identity: carry over the last
132
+ // published meta (model/cwd/persona/approvalMode) just like reset() does,
133
+ // else a resumed session reverts to the empty seed meta (no model name).
134
+ state: {
135
+ ...createSessionViewState(),
136
+ meta: state.meta,
137
+ messages: createMessageStateFromMessages(loaded.messages)
138
+ }
139
+ });
140
+ });
141
+ deps.channel.setToolDecider((request) => {
142
+ const { toolCall, agentId, parentToolCallId, dirAccess } = request;
143
+ const scope = parentToolCallId !== void 0 ? { agentId, parentToolCallId } : void 0;
144
+ const approvalKey = `${agentId}:${toolCall.name}`;
145
+ const auto = !dirAccess && (sudoMode || alwaysApproved.has(approvalKey) || isAutoApproved(toolCall.name));
146
+ if (dirAccess && sudoMode) {
147
+ emit({
148
+ type: "agent-event",
149
+ event: { type: "tool_call", toolCall },
150
+ toolStatus: "approved",
151
+ ...scope ? { scope } : {}
152
+ });
153
+ return { action: "run", grantDir: { dir: dirAccess.proposedDir, persist: false } };
154
+ }
155
+ if (auto) {
156
+ emit({
157
+ type: "agent-event",
158
+ event: { type: "tool_call", toolCall },
159
+ toolStatus: "approved",
160
+ ...scope ? { scope } : {}
161
+ });
162
+ return { action: "run" };
163
+ }
164
+ emit({
165
+ type: "agent-event",
166
+ event: { type: "tool_call", toolCall },
167
+ ...scope ? { scope } : {}
168
+ });
169
+ const nickname = nicknameFor(parentToolCallId);
170
+ const input = asJsonObject(toolCall.input);
171
+ const descriptor = {
172
+ agentId,
173
+ summary: findToolSummary(toolCall.id),
174
+ toolCallId: toolCall.id,
175
+ toolName: toolCall.name,
176
+ ...dirAccess !== void 0 ? { dirAccess } : {},
177
+ ...input !== void 0 ? { input } : {},
178
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {},
179
+ ...nickname !== void 0 ? { nickname } : {}
180
+ };
181
+ return new Promise((resolve) => {
182
+ approvalResolvers.set(toolCall.id, resolve);
183
+ emit({ type: "approval-requested", approval: descriptor });
184
+ });
185
+ });
186
+ deps.channel.setQuestionHandler((request) => {
187
+ questionCounter += 1;
188
+ const questionId = `question-${questionCounter}`;
189
+ return new Promise((resolve, reject) => {
190
+ questionResolvers.set(questionId, { resolve, reject });
191
+ emit({ type: "question-asked", question: { questionId, request } });
192
+ });
193
+ });
194
+ function findToolSummary(toolCallId) {
195
+ const messageId = state.messages.toolMessageIds[toolCallId];
196
+ const message = state.messages.messages.find((entry) => entry.id === messageId);
197
+ return message?.role === "tool" ? message.summary : "";
198
+ }
199
+ function nicknameFor(parentToolCallId) {
200
+ if (!parentToolCallId) return void 0;
201
+ const parentMessageId = state.messages.toolMessageIds[parentToolCallId];
202
+ const parentMessage = state.messages.messages.find((entry) => entry.id === parentMessageId);
203
+ if (parentMessage?.role !== "tool") return void 0;
204
+ const nickname = parentMessage.input?.nickname;
205
+ return typeof nickname === "string" ? nickname : void 0;
206
+ }
207
+ function findDescriptor(toolCallId) {
208
+ return [state.approval.active, ...state.approval.pending].find(
209
+ (entry) => entry?.toolCallId === toolCallId
210
+ );
211
+ }
212
+ function rejectAllPending(reason) {
213
+ for (const [toolCallId, resolve] of approvalResolvers) {
214
+ resolve({ action: "reject", reason });
215
+ emit({ type: "approval-settled", toolCallId, status: "rejected", by: "abort" });
216
+ }
217
+ approvalResolvers.clear();
218
+ for (const [questionId, { reject }] of questionResolvers) {
219
+ reject(new Error(reason));
220
+ emit({ type: "question-settled", questionId });
221
+ }
222
+ questionResolvers.clear();
223
+ }
224
+ function finalizeTurn(controller, reason) {
225
+ if (abortController !== controller) return;
226
+ abortController = null;
227
+ rejectAllPending(reason);
228
+ emitTurnStatus(false);
229
+ const next = queue.shift();
230
+ if (next) {
231
+ if (!next.opts.silent) {
232
+ emit({
233
+ type: "user-message",
234
+ text: next.opts.displayText,
235
+ ...next.opts.attachmentLabels?.length ? { attachmentLabels: next.opts.attachmentLabels } : {}
236
+ });
237
+ }
238
+ void runTurn(next.input, {});
239
+ }
240
+ }
241
+ async function runTurn(input, opts) {
242
+ const controller = new AbortController();
243
+ abortController = controller;
244
+ try {
245
+ emitTurnStatus(true);
246
+ if (!opts.systemInitiated) autoCompactionChain = 0;
247
+ compaction?.setEnabled(true);
248
+ let finalInput = input;
249
+ if (!opts.systemInitiated && guardPrompt) {
250
+ let guarded;
251
+ try {
252
+ guarded = await guardPrompt(input);
253
+ } catch (error) {
254
+ emit({ type: "error", message: `Turn failed: ${errorDetail(error)}` });
255
+ return;
256
+ }
257
+ if ("blocked" in guarded) {
258
+ emit({ type: "error", message: `Prompt blocked: ${guarded.blocked.reason}` });
259
+ return;
260
+ }
261
+ finalInput = guarded.prompt;
262
+ }
263
+ const compactionSend = (text) => runTurn(text, { systemInitiated: true });
264
+ const compactionIo = {
265
+ signal: controller.signal,
266
+ send: compactionSend,
267
+ onInfo: (message) => emit({ type: "info", message }),
268
+ onError: (message) => emit({ type: "error", message })
269
+ };
270
+ const runCompaction = async () => {
271
+ if (!compaction) return;
272
+ if (autoCompactionChain >= MAX_AUTO_COMPACTION_CHAIN) {
273
+ compaction.setEnabled(false);
274
+ emit({
275
+ type: "error",
276
+ message: `Auto-compaction stopped after ${MAX_AUTO_COMPACTION_CHAIN} consecutive compactions without getting under the context limit. Send a new message or use /compact to continue.`
277
+ });
278
+ return;
279
+ }
280
+ autoCompactionChain += 1;
281
+ await compaction.run(compactionIo);
282
+ };
283
+ try {
284
+ await deps.agent.send(finalInput, { streaming: true, signal: controller.signal });
285
+ if (compaction && !controller.signal.aborted) {
286
+ if (compaction.consumePendingAction() === "compact") {
287
+ await runCompaction();
288
+ }
289
+ }
290
+ } catch (error) {
291
+ try {
292
+ const recovered = compaction ? compaction.messagesFromError(error) : null;
293
+ if (recovered) {
294
+ deps.agent.messages = recovered;
295
+ await runCompaction();
296
+ } else if (!controller.signal.aborted && !isAbortError(error)) {
297
+ emit({ type: "error", message: `Turn failed: ${errorDetail(error)}` });
298
+ }
299
+ } catch (recoveryError) {
300
+ emit({ type: "error", message: `Turn failed: ${errorDetail(recoveryError)}` });
301
+ }
302
+ }
303
+ } finally {
304
+ finalizeTurn(controller, "Turn ended before pending interactions could be resolved.");
305
+ }
306
+ }
307
+ return {
308
+ abort() {
309
+ queue.length = 0;
310
+ abortController?.abort(new Error("User aborted the current turn."));
311
+ },
312
+ attachHostHooks(hooks) {
313
+ if (hooks.compaction) compaction = hooks.compaction;
314
+ if (hooks.guardPrompt) guardPrompt = hooks.guardPrompt;
315
+ },
316
+ answerQuestion(questionId, answers) {
317
+ const entry = questionResolvers.get(questionId);
318
+ if (!entry) return false;
319
+ questionResolvers.delete(questionId);
320
+ entry.resolve(answers);
321
+ emit({ type: "question-settled", questionId });
322
+ return true;
323
+ },
324
+ appendError(message) {
325
+ emit({ type: "error", message });
326
+ },
327
+ appendInfo(message) {
328
+ emit({ type: "info", message });
329
+ },
330
+ close() {
331
+ abortController?.abort(new Error("Session closed."));
332
+ rejectAllPending("Session closed.");
333
+ unsubscribeEvents();
334
+ unsubscribeLoaded();
335
+ deps.channel.setToolDecider(null);
336
+ deps.channel.setQuestionHandler(null);
337
+ listeners.clear();
338
+ },
339
+ dequeue(queuedId) {
340
+ const index = queue.findIndex((q) => q.id === queuedId);
341
+ if (index === -1) return false;
342
+ queue.splice(index, 1);
343
+ emitTurnStatus(abortController !== null);
344
+ return true;
345
+ },
346
+ decideApproval(toolCallId, decision, comment, grantDir) {
347
+ const resolve = approvalResolvers.get(toolCallId);
348
+ if (!resolve) return false;
349
+ approvalResolvers.delete(toolCallId);
350
+ const descriptor = findDescriptor(toolCallId);
351
+ if (decision === "a" && descriptor && !descriptor.dirAccess) {
352
+ alwaysApproved.add(`${descriptor.agentId}:${descriptor.toolName}`);
353
+ }
354
+ const rejected = decision === "n";
355
+ if (rejected) {
356
+ const baseReason = `User rejected tool execution (${descriptor?.toolName ?? "unknown tool"})`;
357
+ resolve({ action: "reject", reason: comment ? `${baseReason}: ${comment}` : baseReason });
358
+ } else if (descriptor?.dirAccess) {
359
+ resolve({
360
+ action: "run",
361
+ grantDir: {
362
+ dir: resolveGrantDir(
363
+ descriptor.dirAccess.requestedPath,
364
+ descriptor.dirAccess.proposedDir,
365
+ grantDir
366
+ ),
367
+ persist: decision === "a"
368
+ }
369
+ });
370
+ } else {
371
+ resolve({ action: "run" });
372
+ }
373
+ emit({
374
+ type: "approval-settled",
375
+ toolCallId,
376
+ status: rejected ? "rejected" : "approved",
377
+ by: "client"
378
+ });
379
+ return true;
380
+ },
381
+ echoUserMessage(text, attachmentLabels) {
382
+ emit({
383
+ type: "user-message",
384
+ text,
385
+ ...attachmentLabels?.length ? { attachmentLabels } : {}
386
+ });
387
+ },
388
+ getSnapshot() {
389
+ return { seq, state, serverCommands };
390
+ },
391
+ onEvent(listener) {
392
+ listeners.add(listener);
393
+ return () => listeners.delete(listener);
394
+ },
395
+ publishSessionMeta(meta) {
396
+ emit({ type: "session-meta", meta });
397
+ },
398
+ publishMetrics(snapshot) {
399
+ emit({ type: "metrics", snapshot });
400
+ },
401
+ publishAuthFlow(flow) {
402
+ emit({ type: "auth-flow", flow });
403
+ },
404
+ publishWorkflowProgress(runId, delta) {
405
+ emit({ type: "workflow-progress", runId, delta });
406
+ },
407
+ reset() {
408
+ queue.length = 0;
409
+ abortController?.abort(new Error("Session reset."));
410
+ rejectAllPending("Session reset; pending interactions were cancelled.");
411
+ alwaysApproved.clear();
412
+ sudoMode = false;
413
+ emit({
414
+ type: "state-reset",
415
+ state: { ...createSessionViewState(), meta: state.meta }
416
+ });
417
+ },
418
+ setSudoMode(value) {
419
+ sudoMode = value;
420
+ },
421
+ async submit(input, opts) {
422
+ if (abortController) {
423
+ queue.push({ id: `q-${++queueCounter}`, input, opts });
424
+ emitTurnStatus(true);
425
+ return;
426
+ }
427
+ if (!opts.silent) {
428
+ emit({
429
+ type: "user-message",
430
+ text: opts.displayText,
431
+ ...opts.attachmentLabels?.length ? { attachmentLabels: opts.attachmentLabels } : {}
432
+ });
433
+ }
434
+ await runTurn(input, {});
435
+ }
436
+ };
437
+ }
438
+ export {
439
+ AgentChannel,
440
+ createSessionController
441
+ };
@@ -0,0 +1,147 @@
1
+ import type { AgentMetricsSnapshot } from "@nuvin/agent-core/agent";
2
+ import { type AgentInput, type AskUserAnswers, type Message } from "@nuvin/agent-core/shared";
3
+ import type { McpAuthFlowState, ServerEvent, SessionMeta, SessionSnapshot, SlashCommandDescriptor, WorkflowProgressDelta } from "../protocol/types.ts";
4
+ import type { AgentChannel } from "./agent-channel.ts";
5
+ /** Subset of Agent the controller needs — structurally satisfied by @nuvin/agent-core Agent. */
6
+ export type AgentLike = {
7
+ messages: Message[];
8
+ send(input: AgentInput, opts: {
9
+ streaming: boolean;
10
+ signal: AbortSignal;
11
+ }): Promise<unknown>;
12
+ };
13
+ export type GuardResult = {
14
+ blocked: {
15
+ reason: string;
16
+ };
17
+ } | {
18
+ prompt: AgentInput;
19
+ };
20
+ /**
21
+ * Compaction hooks, adapted from packages/cli/src/lib/chat/context-compaction.ts
22
+ * by the runtime assembly. Optional: absent in tests and minimal setups.
23
+ */
24
+ export type CompactionHooks = {
25
+ /** Called at turn start so monitor observers can be muted for system turns. */
26
+ setEnabled(enabled: boolean): void;
27
+ /** After a successful turn: the action the monitor decided on. */
28
+ consumePendingAction(): "compact" | "none" | "warn";
29
+ /** If `error` demands compaction, return the recoverable messages, else null. */
30
+ messagesFromError(error: unknown): Message[] | null;
31
+ /** Compact, then resume by calling `send` (re-enters runTurn system-initiated). */
32
+ run(opts: {
33
+ signal: AbortSignal;
34
+ send: (text: string) => Promise<void>;
35
+ onInfo: (message: string) => void;
36
+ onError: (message: string) => void;
37
+ }): Promise<void>;
38
+ };
39
+ export interface SessionControllerDeps {
40
+ agent: AgentLike;
41
+ channel: AgentChannel;
42
+ compaction?: CompactionHooks;
43
+ /** UserPromptSubmit hook gate; absent = no guarding (test/dev mode). */
44
+ guardPrompt?: (input: AgentInput) => Promise<GuardResult>;
45
+ /** Defaults to isAutoApprovedTool from ../state. */
46
+ isAutoApproved?: (toolName: string) => boolean;
47
+ /** Injectable clock for deterministic tests. Defaults to Date.now. */
48
+ now?: () => number;
49
+ /** Format an unknown error for the transcript. Defaults to String(error). */
50
+ errorDetail?: (error: unknown) => string;
51
+ /**
52
+ * Descriptors for server-executed slash commands, surfaced to clients via the
53
+ * attach snapshot for autocomplete. Defaults to an empty list.
54
+ */
55
+ serverCommands?: SlashCommandDescriptor[];
56
+ }
57
+ export type SubmitOptions = {
58
+ displayText: string;
59
+ attachmentLabels?: string[];
60
+ /**
61
+ * When true, submit emits no `user-message` event (slash-initiated turns
62
+ * already echoed their text). `displayText` may be `""` in that case.
63
+ */
64
+ silent?: boolean;
65
+ };
66
+ export interface SessionController {
67
+ abort(): void;
68
+ /**
69
+ * Attach runtime-scoped hooks the daemon's session host owns. The host is
70
+ * built after the controller (it needs the controller), so compaction +
71
+ * guardPrompt are wired in here once the single real host exists. Provided
72
+ * hooks replace any construction-time values; `undefined` fields are ignored.
73
+ * Bind exactly one host per controller — this is what keeps a single,
74
+ * controller-bound compaction monitor (no duplicate, uncontrolled monitor).
75
+ */
76
+ attachHostHooks(hooks: {
77
+ compaction?: CompactionHooks;
78
+ guardPrompt?: (input: AgentInput) => Promise<GuardResult>;
79
+ }): void;
80
+ /**
81
+ * Settle a pending question by id.
82
+ *
83
+ * Single-active-question is the current protocol invariant: when parallel
84
+ * sub-agents ask concurrently, only the latest question is surfaced as
85
+ * `question.active` in state. All resolvers survive in the internal map, so
86
+ * answering any questionId settles the matching ask correctly.
87
+ */
88
+ answerQuestion(questionId: string, answers: AskUserAnswers): boolean;
89
+ /** Emit an error line into the replicated transcript. */
90
+ appendError(message: string): void;
91
+ /** Emit an info line into the replicated transcript (slash output, system notices). */
92
+ appendInfo(message: string): void;
93
+ close(): void;
94
+ /**
95
+ * Cancel a queued (not-yet-running) submission by its `turn-status.queued` id.
96
+ * Returns true if an item was removed, false if the id was unknown (already
97
+ * drained or never existed) — a race-safe no-op the host still acks ok.
98
+ */
99
+ dequeue(queuedId: string): boolean;
100
+ decideApproval(toolCallId: string, decision: "a" | "n" | "y", comment?: string, grantDir?: string): boolean;
101
+ /** Echo a user-style message without starting a turn (slash commands echo their invocation). */
102
+ echoUserMessage(text: string, attachmentLabels?: string[]): void;
103
+ /**
104
+ * Returns the live internal state reference (NOT a deep copy). This is safe
105
+ * only because every reducer is copy-on-write: past snapshots are never
106
+ * mutated in place. Callers must treat the returned state as read-only.
107
+ */
108
+ getSnapshot(): SessionSnapshot;
109
+ onEvent(listener: (event: ServerEvent) => void): () => void;
110
+ /** Publish display metadata (model name, cwd, approval mode). Host calls on config changes. */
111
+ publishSessionMeta(meta: SessionMeta): void;
112
+ /**
113
+ * Publish a live agent-metrics snapshot (spec §4.3). The host throttles these
114
+ * to ~1/s during a turn; the controller just sequences + reduces them.
115
+ */
116
+ publishMetrics(snapshot: AgentMetricsSnapshot): void;
117
+ /**
118
+ * Publish an MCP OAuth login flow update (spec §10/§12). The host calls this
119
+ * from its `McpAuthController` subscription; the controller sequences +
120
+ * reduces it like any other event. `null` clears the client modal.
121
+ */
122
+ publishAuthFlow(flow: McpAuthFlowState): void;
123
+ /**
124
+ * Publish a workflow UI progress delta (spec §8.2/§13). The WorkflowManager
125
+ * is the sole caller. emit-only — sequences + reduces into
126
+ * `state.workflows[runId]`; NEVER calls submit, never touches agent messages.
127
+ */
128
+ publishWorkflowProgress(runId: string, delta: WorkflowProgressDelta): void;
129
+ /**
130
+ * Reset session state: abort any active turn, clear queued submissions,
131
+ * reject pending approvals/questions, clear alwaysApproved + sudo, and emit
132
+ * a state-reset with a fresh view state CARRYING OVER the last published
133
+ * meta. Runtime-side resets (sessionStore.startNewSession, agent system
134
+ * prompt, metrics) are the HOST's job — it calls those, then this.
135
+ */
136
+ reset(): void;
137
+ setSudoMode(value: boolean): void;
138
+ /**
139
+ * Submit user input. If no turn is active, the returned promise resolves
140
+ * when the turn completes. If a turn is already active, the submission is
141
+ * queued and the promise resolves at ENQUEUE time — remote transports should
142
+ * ack on acceptance and observe completion via `turn-status` events.
143
+ */
144
+ submit(input: AgentInput, opts: SubmitOptions): Promise<void>;
145
+ }
146
+ export declare function createSessionController(deps: SessionControllerDeps): SessionController;
147
+ //# sourceMappingURL=session-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-controller.d.ts","sourceRoot":"","sources":["../../src/controller/session-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { ServerEvent } from "../protocol/types.ts";
2
+ import { AgentChannel } from "./agent-channel.ts";
3
+ import type { SessionControllerDeps } from "./session-controller.ts";
4
+ export declare function makeDeps(overrides?: Partial<SessionControllerDeps>): {
5
+ channel: AgentChannel;
6
+ deps: SessionControllerDeps;
7
+ send: import("vitest").Mock<(_input: unknown, _opts: {
8
+ streaming: boolean;
9
+ signal: AbortSignal;
10
+ }) => Promise<void>>;
11
+ };
12
+ export declare function collectEvents(controller: {
13
+ onEvent(l: (e: ServerEvent) => void): () => void;
14
+ }): ServerEvent[];
15
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../src/controller/test-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,wBAAgB,QAAQ,CAAC,SAAS,GAAE,OAAO,CAAC,qBAAqB,CAAM;;;yCAGpD,OAAO,SAAS;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE;EAa7E;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE;IAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;CAAE,iBAI7F"}
@@ -0,0 +1,33 @@
1
+ import type { GrantPayload } from "../protocol/types.ts";
2
+ /** ±5 min clock-skew tolerance on expiry/issuedAt checks (relay spec §7). */
3
+ export declare const DEFAULT_GRANT_SKEW_MS: number;
4
+ /** base64 DER key pair: `publicKey` SPKI (daemon.yaml), `privateKey` PKCS#8 (server secret). */
5
+ export type GrantKeyPair = {
6
+ publicKey: string;
7
+ privateKey: string;
8
+ };
9
+ export type GrantVerifyFailure = "daemon-mismatch" | "expired" | "malformed" | "not-yet-valid" | "signature";
10
+ export type GrantVerifyResult = {
11
+ ok: true;
12
+ payload: GrantPayload;
13
+ } | {
14
+ ok: false;
15
+ reason: GrantVerifyFailure;
16
+ };
17
+ export declare function generateGrantKeyPair(): GrantKeyPair;
18
+ /** Mint the wire form of a grant. Server-side (and tests); daemons only verify. */
19
+ export declare function signGrant(payload: GrantPayload, privateKeyBase64: string): string;
20
+ /**
21
+ * Verify a grant (relay spec §7, daemon-side): signature valid → daemonId is
22
+ * mine → expiresAt in the future and issuedAt not absurdly in the future
23
+ * (±skew). Never throws — every failure maps to a reason the caller turns
24
+ * into the existing `rejected {reason:"auth"}` path. Signature is checked
25
+ * BEFORE the payload is parsed so unsigned bytes are never trusted.
26
+ */
27
+ export declare function verifyGrant(grant: string, opts: {
28
+ publicKey: string;
29
+ daemonId: string;
30
+ now?: number;
31
+ skewMs?: number;
32
+ }): GrantVerifyResult;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grant/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,QAAgB,CAAC;AAEnD,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAC1B,iBAAiB,GACjB,SAAS,GACT,WAAW,GACX,eAAe,GACf,WAAW,CAAC;AAEhB,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE9C,wBAAgB,oBAAoB,IAAI,YAAY,CAMnD;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CASjF;AAcD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3E,iBAAiB,CAuCnB"}
@@ -0,0 +1,12 @@
1
+ import {
2
+ DEFAULT_GRANT_SKEW_MS,
3
+ generateGrantKeyPair,
4
+ signGrant,
5
+ verifyGrant
6
+ } from "../chunk-3IPZO7LP.js";
7
+ export {
8
+ DEFAULT_GRANT_SKEW_MS,
9
+ generateGrantKeyPair,
10
+ signGrant,
11
+ verifyGrant
12
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./types.ts";
2
+ export * from "./version.ts";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ import "../chunk-DGC7JSCG.js";
2
+ import {
3
+ PROTOCOL_VERSION
4
+ } from "../chunk-7G3R25NS.js";
5
+ export {
6
+ PROTOCOL_VERSION
7
+ };