@jini-ai/daemon 0.6.15 → 0.6.17

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 (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -4
  3. package/dist/agent-executor/codex/isolation.js +2 -3
  4. package/dist/agent-executor/contracts.d.ts +1 -1
  5. package/dist/agent-executor/contracts.js +0 -1
  6. package/dist/agent-executor/event-translation.d.ts +3 -3
  7. package/dist/agent-executor/event-translation.js +4 -4
  8. package/dist/agent-executor/events.js +1 -3
  9. package/dist/agent-executor/index.d.ts +5 -5
  10. package/dist/agent-executor/index.js +5 -5
  11. package/dist/ai-trace-lifecycle.d.ts +1 -1
  12. package/dist/ai-trace-lifecycle.js +1 -1
  13. package/dist/core/tool-audit.js +11 -7
  14. package/dist/frontend-capability-tools.d.ts +2 -0
  15. package/dist/frontend-capability-tools.js +7 -1
  16. package/dist/frontend-session-registry.d.ts +42 -3
  17. package/dist/frontend-session-registry.js +61 -9
  18. package/dist/http/frontend-control.d.ts +13 -0
  19. package/dist/http/frontend-control.js +14 -3
  20. package/dist/http/frontend-guest-scope.d.ts +32 -0
  21. package/dist/http/frontend-guest-scope.js +63 -0
  22. package/dist/http/frontend-sessions.d.ts +29 -6
  23. package/dist/http/frontend-sessions.js +106 -28
  24. package/dist/http/index.d.ts +1 -0
  25. package/dist/http/index.js +1 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +2 -2
  28. package/dist/store/event-log/sqlite-kernel.js +2 -1
  29. package/dist/terminal-session.d.ts +51 -20
  30. package/dist/terminal-session.js +6 -89
  31. package/dist/terminal.adapter.d.ts +22 -0
  32. package/dist/terminal.adapter.js +47 -0
  33. package/dist/tool-executor.d.ts +4 -2
  34. package/dist/tool-executor.js +50 -14
  35. package/dist/tool-result-media.d.ts +4 -0
  36. package/dist/tool-result-media.js +3 -2
  37. package/package.json +29 -38
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Opt-in native PTY adapter. Import from `@jini-ai/daemon/terminal` and inject
3
+ * `() => loadRealSpawnPty({})` into the session manager's `loadSpawnPty` port.
4
+ */
5
+ import { loadOptionalPeer } from '@jini-ai/core/optional-peer';
6
+ import { createRequire } from 'node:module';
7
+ import { ensureSpawnHelperExecutable, spawnHelperCandidatePaths, } from '@jini-ai/platform';
8
+ const daemonRequire = createRequire(import.meta.url);
9
+ /** Adapts a real `node-pty` `IPty` to `@jini-ai/platform`'s narrower `PtyProcess` port — explicit per-member mapping rather than relying on structural assignability, matching this package's established "explicit adapter, no implicit coercion" convention (e.g. `agent-executor.ts`'s `toStringEnvRecord`). */
10
+ function toPtyProcess(pty) {
11
+ return {
12
+ onData: (cb) => {
13
+ pty.onData(cb);
14
+ },
15
+ onExit: (cb) => {
16
+ pty.onExit(cb);
17
+ },
18
+ write: (input) => pty.write(input),
19
+ resize: (cols, rows) => pty.resize(cols, rows),
20
+ kill: (signal) => pty.kill(signal),
21
+ };
22
+ }
23
+ /**
24
+ * The real, `node-pty`-backed {@link PtySpawn}. Dynamically imports
25
+ * `node-pty` (mirroring OD's origin `await import('node-pty')`, not a
26
+ * static top-level import) so a host process whose platform/environment
27
+ * lacks a usable `node-pty` addon can still boot — only an actual
28
+ * `terminal.create` call fails, cleanly, rather than crashing at
29
+ * module-eval time. Before the first spawn, repairs `node-pty`'s bundled
30
+ * `spawn-helper` binary's executable bit (pnpm unpacks it non-executable —
31
+ * see `@jini-ai/platform`'s `terminal.ts` module doc for the full story) via
32
+ * that package's own `spawnHelperCandidatePaths`/`ensureSpawnHelperExecutable`,
33
+ * but with the resolver anchored at **this** module
34
+ * (`createRequire(import.meta.url)` here, not `@jini-ai/platform`'s own
35
+ * default): `@jini-ai/platform` deliberately does not depend on `node-pty`, so
36
+ * its default `require.resolve('node-pty')` would not find the copy this
37
+ * package (`@jini-ai/daemon`) actually declares — this is exactly the
38
+ * resolver injection seam `spawnHelperCandidatePaths` was built for.
39
+ * @throws A feature-specific load error with the original resolution/ABI error as `cause`.
40
+ * @complexity O(1) adapter work; module loading and helper repair belong to the platform.
41
+ */
42
+ export async function loadRealSpawnPty(_requiredArgs, _optionalArgs = {}) {
43
+ ensureSpawnHelperExecutable(spawnHelperCandidatePaths({ resolve: (specifier) => daemonRequire.resolve(specifier) }));
44
+ const nodePty = await loadOptionalPeer({ feature: 'Interactive terminals', dependency: 'node-pty', load: () => import('node-pty') });
45
+ return (shell, args, options) => toPtyProcess(nodePty.spawn(shell, args, options));
46
+ }
47
+ //# sourceMappingURL=terminal.adapter.js.map
@@ -17,7 +17,7 @@ export interface ToolConfirmationRequest {
17
17
  * The transport-supplied seam for the two touchpoints a headless kernel
18
18
  * cannot render itself. Both are optional: a tool whose `ToolPolicy`
19
19
  * always resolves definitively needs no `onAuthorize`, and a tool with
20
- * `requiresConfirmation` unset never triggers `onConfirm`.
20
+ * `requiresConfirmation` unset has no default prompt; a host may strengthen a single call with `requireConfirmation: true`.
21
21
  */
22
22
  export interface ExecutionDelegate {
23
23
  /**
@@ -42,7 +42,7 @@ export interface ExecutionDelegate {
42
42
  */
43
43
  onConfirm?(request: ToolConfirmationRequest): ConfirmationDecision | Promise<ConfirmationDecision> | void;
44
44
  }
45
- export type ToolExecutionPhase = 'requested' | 'authorized' | 'denied' | 'confirmed' | 'confirmation-denied' | 'started' | 'completed' | 'failed' | 'timed-out' | 'cancelled';
45
+ export type ToolExecutionPhase = 'requested' | 'authorized' | 'denied' | 'confirmed' | 'approval-recorded' | 'confirmation-denied' | 'started' | 'completed' | 'failed' | 'timed-out' | 'cancelled';
46
46
  export interface ToolExecutionAuditEvent {
47
47
  readonly phase: ToolExecutionPhase;
48
48
  readonly at: number;
@@ -85,6 +85,7 @@ export interface ToolExecutor {
85
85
  * @param emitSurface - Transport-supplied seam letting the handler push a human-only surface into
86
86
  * the run's event stream *before* this call settles. Omit for a caller with no run event stream;
87
87
  * see `@jini-ai/core`'s `SurfaceEmitter` for why a handler that waits on a human needs it.
88
+ * @param requireConfirmation - Strengthens this call only; cannot bypass a descriptor confirmation.
88
89
  * @throws {ToolInputError} If `toolId` isn't registered, with up to eight registered prefix matches.
89
90
  */
90
91
  execute(args: {
@@ -95,6 +96,7 @@ export interface ToolExecutor {
95
96
  }, optionalArgs?: {
96
97
  readonly signal?: AbortSignal;
97
98
  readonly emitSurface?: SurfaceEmitter;
99
+ readonly requireConfirmation?: true;
98
100
  }): Promise<ToolExecutionResult>;
99
101
  /** @throws If `executionId` has no confirmation currently pending (already resumed, never required one, or unknown). */
100
102
  resumeConfirmation(args: {
@@ -46,7 +46,7 @@
46
46
  * answer gets back in.
47
47
  */
48
48
  import { randomUUID } from 'node:crypto';
49
- import { ToolInputError, withNormalizationNotes, ToolInputNormalizationError, isReadOnlyTool, } from '@jini-ai/core';
49
+ import { ToolInputError, APPROVAL_WAIT_TIMEOUT_MS, withNormalizationNotes, ToolInputNormalizationError, isReadOnlyTool, } from '@jini-ai/core';
50
50
  import { authorizeToolInvocation } from '@jini-ai/core/composition';
51
51
  import { toolCandidates } from './core/tool-candidates.js';
52
52
  /**
@@ -206,15 +206,45 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
206
206
  * what distinguishes a `timed-out` result from a `cancelled` one.
207
207
  */
208
208
  function startTimeout(controller, timeoutMs) {
209
- if (timeoutMs === undefined)
210
- return { handle: undefined, timedOut: () => false };
209
+ let handle;
211
210
  let timedOut = false;
212
- const handle = setTimeout(() => {
213
- timedOut = true;
214
- controller.abort();
215
- }, timeoutMs);
216
- handle.unref?.();
217
- return { handle, timedOut: () => timedOut };
211
+ let waiting = false;
212
+ let approvalWindowUsed = false;
213
+ let remaining = timeoutMs;
214
+ let started = Date.now();
215
+ const stop = () => {
216
+ if (handle === undefined)
217
+ return;
218
+ clearTimeout(handle);
219
+ handle = undefined;
220
+ };
221
+ const arm = (duration) => {
222
+ stop();
223
+ started = Date.now();
224
+ if (duration === undefined)
225
+ return;
226
+ handle = setTimeout(() => { timedOut = true; controller.abort(); }, duration);
227
+ handle.unref?.();
228
+ };
229
+ arm(remaining);
230
+ return {
231
+ stop,
232
+ timedOut: () => timedOut,
233
+ /** Human waiting is bounded independently and cannot replenish the execution budget.
234
+ * Duplicate progress frames cannot extend either deadline. */
235
+ approval: ({ pending }) => {
236
+ if (pending === waiting || controller.signal.aborted)
237
+ return;
238
+ if (pending && approvalWindowUsed)
239
+ return;
240
+ if (pending)
241
+ approvalWindowUsed = true;
242
+ if (pending && remaining !== undefined)
243
+ remaining = Math.max(0, remaining - (Date.now() - started));
244
+ waiting = pending;
245
+ arm(pending ? APPROVAL_WAIT_TIMEOUT_MS : remaining);
246
+ },
247
+ };
218
248
  }
219
249
  /** Records a terminal transition that carries no detail and returns its bare result. */
220
250
  function terminal({ executionId, status }) {
@@ -406,8 +436,8 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
406
436
  // the catch). Repeating one call avoids leaving an uncoverable branch
407
437
  // behind instead of writing a contrived test or suppressing it.
408
438
  const cleanup = () => {
409
- if (timeout.handle)
410
- clearTimeout(timeout.handle);
439
+ timeout.stop();
440
+ approvalStateChange({ run, executionId, toolId: call.toolId, pending: false });
411
441
  };
412
442
  appendEvent(executionId, 'started');
413
443
  try {
@@ -422,7 +452,13 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
422
452
  signal: controller.signal,
423
453
  }, {
424
454
  ...surfaceOption(emitSurface),
425
- onApprovalStateChange: ({ pending }) => approvalStateChange({ run, executionId, toolId: call.toolId, pending }),
455
+ onApprovalStateChange: ({ pending, decision }) => {
456
+ // The tab owns human confirmation; retain its outcome without opening a chat surface.
457
+ if (decision)
458
+ appendEvent(executionId, 'approval-recorded', `frontend approval: ${decision}`);
459
+ timeout.approval({ pending });
460
+ approvalStateChange({ run, executionId, toolId: call.toolId, pending });
461
+ },
426
462
  invokeTool: nestedInvoker({ principal, run, toolId: call.toolId, invocationRegistry: call.invocationRegistry, signal: controller.signal }, { emitSurface }),
427
463
  });
428
464
  cleanup();
@@ -433,7 +469,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
433
469
  return failureResult({ executionId, err, timedOut: timeout.timedOut(), aborted: controller.signal.aborted }, { normalizationNotes, guidance });
434
470
  }
435
471
  }
436
- async function execute({ principal, run, toolId, input }, { signal, emitSurface, invocationRegistry = registry } = {}) {
472
+ async function execute({ principal, run, toolId, input }, { signal, emitSurface, invocationRegistry = registry, requireConfirmation } = {}) {
437
473
  // Existence is checked separately from — and before — authorization so the audit record can
438
474
  // be opened before the (possibly slow, possibly throwing) authorization gate runs, matching
439
475
  // pre-authorizeToolInvocation timing: `requested` used to precede `policy.authorize`, and a
@@ -494,7 +530,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
494
530
  }
495
531
  const admitted = gate.allowed;
496
532
  const { descriptor } = admitted;
497
- if (descriptor.requiresConfirmation) {
533
+ if (descriptor.requiresConfirmation || requireConfirmation) {
498
534
  const declined = await confirmGate({ executionId, principal, run, tool: descriptor, input: admitted.input });
499
535
  if (declined) {
500
536
  cleanupCancellation();
@@ -42,6 +42,10 @@ export interface ToolResultImageBlock {
42
42
  readonly mimeType: string;
43
43
  /** Base64-encoded image bytes. */
44
44
  readonly data: string;
45
+ /** Optional caption and alt text for a chat renderer. Not an MCP field; MCP clients ignore it. */
46
+ readonly title?: string;
47
+ /** Optional link the chat opens on click (e.g. the full-size file). The renderer still checks it. */
48
+ readonly href?: string;
45
49
  }
46
50
  /** A tool result content block `media` may carry. Modeled on MCP's own content-block vocabulary —
47
51
  * see this module's doc for why that vocabulary is reused rather than a fourth one invented. */
@@ -8,10 +8,11 @@ function isRecord(value) {
8
8
  function asImageBlock(block) {
9
9
  if (!isRecord(block) || block['type'] !== 'image')
10
10
  return undefined;
11
- const { mimeType, data } = block;
11
+ const { mimeType, data, title, href } = block;
12
12
  if (typeof mimeType !== 'string' || typeof data !== 'string')
13
13
  return undefined;
14
- return { type: 'image', mimeType, data };
14
+ // Untrusted extras are kept only as strings, so a caption or link can never smuggle an object.
15
+ return { type: 'image', mimeType, data, ...(typeof title === 'string' ? { title } : {}), ...(typeof href === 'string' ? { href } : {}) };
15
16
  }
16
17
  /**
17
18
  * Splits a tool handler's raw return value into its recognized media blocks and the remainder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "runtime": "node",
15
15
  "entries": {
16
16
  ".": "node",
17
+ "./terminal": "node",
17
18
  "./store/event-log/sqlite": "node",
18
19
  "./store/agent-sessions": "universal",
19
20
  "./store/agent-sessions/sqlite": "node",
@@ -34,6 +35,12 @@
34
35
  "src/surface-exchanges.ts",
35
36
  "src/scheduler.ts"
36
37
  ]
38
+ },
39
+ "optionalPeers": {
40
+ "node-pty": "opt-in: import @jini-ai/daemon/terminal — Interactive terminals could not load node-pty. Install node-pty to enable this feature, or check its installed version and native binaries.",
41
+ "better-sqlite3": "opt-in: sqlite storage — SQLite storage could not load better-sqlite3. Install better-sqlite3 to enable this feature, or check its installed version and native binaries.",
42
+ "@electric-sql/pglite": "opt-in: embedded postgresql storage — Embedded PostgreSQL storage could not load @electric-sql/pglite. Install @electric-sql/pglite to enable this feature, or check its installed version and native binaries.",
43
+ "pg": "opt-in: postgresql storage — PostgreSQL storage could not load pg. Install pg to enable this feature, or check its installed version and native binaries."
37
44
  }
38
45
  },
39
46
  "type": "module",
@@ -46,6 +53,11 @@
46
53
  "import": "./dist/index.js",
47
54
  "default": "./dist/index.js"
48
55
  },
56
+ "./terminal": {
57
+ "types": "./dist/terminal.adapter.d.ts",
58
+ "import": "./dist/terminal.adapter.js",
59
+ "default": "./dist/terminal.adapter.js"
60
+ },
49
61
  "./store/event-log/sqlite": {
50
62
  "types": "./dist/store/event-log/sqlite.d.ts",
51
63
  "import": "./dist/store/event-log/sqlite.js",
@@ -140,44 +152,35 @@
140
152
  "@types/pg": "8.23.1",
141
153
  "express": "^4.21.0",
142
154
  "@types/express": "^4.17.21",
143
- "@jini-ai/db": "0.6.15",
144
- "@jini-ai/http-kit": "0.6.15",
145
- "@jini-ai/chat": "0.6.15",
146
- "@jini-ai/core": "^0.6.15",
147
- "@jini-ai/platform": "^0.6.15",
148
- "@jini-ai/protocol": "^0.6.15",
149
- "@jini-ai/agent-runtime": "^0.6.15"
155
+ "@jini-ai/http-kit": "0.6.17",
156
+ "@jini-ai/chat": "0.6.17",
157
+ "@jini-ai/db": "0.6.17",
158
+ "@jini-ai/core": "^0.6.17",
159
+ "@jini-ai/agent-runtime": "^0.6.17",
160
+ "@jini-ai/platform": "^0.6.17",
161
+ "@jini-ai/protocol": "^0.6.17"
150
162
  },
151
163
  "dependencies": {
152
- "@jini-ai/core": "^0.6.15",
153
- "@jini-ai/protocol": "^0.6.15"
164
+ "smol-toml": "1.9.1",
165
+ "kysely": "^0.29.6",
166
+ "@jini-ai/core": "^0.6.17",
167
+ "@jini-ai/db": "^0.6.17",
168
+ "@jini-ai/platform": "^0.6.17",
169
+ "@jini-ai/agent-runtime": "^0.6.17",
170
+ "@jini-ai/protocol": "^0.6.17",
171
+ "@jini-ai/http-kit": "^0.6.17"
154
172
  },
155
173
  "peerDependencies": {
156
- "smol-toml": "1.9.1",
157
174
  "node-pty": "^1.1.0",
158
- "@jini-ai/db": "^0.6.10",
159
- "kysely": "^0.29.6",
160
175
  "better-sqlite3": "^13.0.0",
161
176
  "@electric-sql/pglite": "0.5.8",
162
177
  "pg": "^8.23.0",
163
- "@jini-ai/http-kit": "^0.6.10",
164
- "express": "^4.21.0 || ^5.0.0",
165
- "@jini-ai/agent-runtime": "^0.6.10",
166
- "@jini-ai/platform": "^0.6.10"
178
+ "express": "^4.21.0 || ^5.0.0"
167
179
  },
168
180
  "peerDependenciesMeta": {
169
- "smol-toml": {
170
- "optional": true
171
- },
172
181
  "node-pty": {
173
182
  "optional": true
174
183
  },
175
- "@jini-ai/db": {
176
- "optional": true
177
- },
178
- "kysely": {
179
- "optional": true
180
- },
181
184
  "better-sqlite3": {
182
185
  "optional": true
183
186
  },
@@ -186,18 +189,6 @@
186
189
  },
187
190
  "pg": {
188
191
  "optional": true
189
- },
190
- "@jini-ai/http-kit": {
191
- "optional": true
192
- },
193
- "express": {
194
- "optional": true
195
- },
196
- "@jini-ai/agent-runtime": {
197
- "optional": true
198
- },
199
- "@jini-ai/platform": {
200
- "optional": true
201
192
  }
202
193
  },
203
194
  "scripts": {