@jini-ai/daemon 0.6.12 → 0.6.15-rc.1

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 (56) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent-executor/codex/home-config.d.ts +50 -0
  3. package/dist/agent-executor/codex/home-config.js +209 -0
  4. package/dist/agent-executor/codex/home-staging.d.ts +60 -0
  5. package/dist/agent-executor/codex/home-staging.js +195 -0
  6. package/dist/agent-executor/codex/isolation.d.ts +27 -0
  7. package/dist/agent-executor/codex/isolation.js +67 -0
  8. package/dist/agent-executor/contracts.d.ts +4 -1
  9. package/dist/agent-executor/event-translation.d.ts +7 -7
  10. package/dist/agent-executor/event-translation.js +17 -17
  11. package/dist/agent-executor/events.js +1 -1
  12. package/dist/agent-executor/index.d.ts +3 -1
  13. package/dist/agent-executor/index.js +3 -1
  14. package/dist/agent-executor/launch.d.ts +3 -2
  15. package/dist/agent-executor/launch.js +68 -17
  16. package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
  17. package/dist/agent-executor/mcp-bridge-probe.js +1 -1
  18. package/dist/agent-executor/mcp-bridge.d.ts +9 -86
  19. package/dist/agent-executor/mcp-bridge.js +4 -355
  20. package/dist/ai-trace-lifecycle.d.ts +48 -0
  21. package/dist/ai-trace-lifecycle.js +116 -0
  22. package/dist/core/read-only-tools.js +3 -1
  23. package/dist/delegated-tool-bridge.js +19 -1
  24. package/dist/http/attachment-lookup-routes.d.ts +44 -0
  25. package/dist/http/attachment-lookup-routes.js +71 -0
  26. package/dist/http/attachments.d.ts +112 -7
  27. package/dist/http/attachments.js +58 -17
  28. package/dist/http/connectors.js +23 -18
  29. package/dist/http/index.d.ts +6 -0
  30. package/dist/http/index.js +3 -0
  31. package/dist/http/tool-execution-routes.d.ts +69 -0
  32. package/dist/http/tool-execution-routes.js +181 -0
  33. package/dist/http/xai.js +19 -9
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +3 -0
  36. package/dist/remote-attachment-lookup.d.ts +29 -0
  37. package/dist/remote-attachment-lookup.js +71 -0
  38. package/dist/remote-tool-executor.d.ts +85 -0
  39. package/dist/remote-tool-executor.js +250 -0
  40. package/dist/routing-tool-executor.d.ts +29 -0
  41. package/dist/routing-tool-executor.js +16 -0
  42. package/dist/run-lifecycle.d.ts +4 -1
  43. package/dist/run-lifecycle.js +35 -5
  44. package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
  45. package/dist/store/event-log/sqlite-kernel.js +135 -0
  46. package/dist/store/event-log/sqlite-policy.d.ts +93 -0
  47. package/dist/store/event-log/sqlite-policy.js +74 -0
  48. package/dist/store/event-log/sqlite.d.ts +7 -31
  49. package/dist/store/event-log/sqlite.js +32 -95
  50. package/dist/tool-recovery/analytics.d.ts +27 -0
  51. package/dist/tool-recovery/analytics.js +38 -0
  52. package/dist/tool-recovery/index.d.ts +2 -0
  53. package/dist/tool-recovery/index.js +2 -0
  54. package/dist/tool-recovery/observability.d.ts +82 -0
  55. package/dist/tool-recovery/observability.js +88 -0
  56. package/package.json +15 -14
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Default per-run retention cap applied when `SqliteEventLogOptions.maxEntriesPerRun` is
3
+ * omitted — see that option's doc for why a durable adapter defaults to a bound instead of
4
+ * mirroring the in-memory reference adapter's unbounded-by-default behavior.
5
+ */
6
+ export const DEFAULT_MAX_ENTRIES_PER_RUN = 2000;
7
+ /** @internal Validates and resolves `options.maxEntriesPerRun`, applying the documented default. */
8
+ export function resolveMaxEntriesPerRun(value) {
9
+ if (value === undefined)
10
+ return DEFAULT_MAX_ENTRIES_PER_RUN;
11
+ if (!Number.isSafeInteger(value) || value < 0) {
12
+ throw new Error(`createSqliteEventLog: options.maxEntriesPerRun must be a non-negative safe integer, got ${value}`);
13
+ }
14
+ return value;
15
+ }
16
+ /** @internal Validates a `replay(runId, afterCursor)` cursor. `null` ("from the beginning") is always valid. */
17
+ export function assertValidReplayCursor(afterCursor) {
18
+ if (afterCursor === null)
19
+ return;
20
+ const n = Number(afterCursor);
21
+ if (!Number.isSafeInteger(n) || n < 0) {
22
+ throw new Error(`createSqliteEventLog: invalid replay cursor ${JSON.stringify(afterCursor)} — must be a non-negative safe integer string`);
23
+ }
24
+ }
25
+ /** Decode the persisted JSON boundary while preserving the original cursor and timestamp. */
26
+ export function rowToEntry(row) {
27
+ return {
28
+ id: String(row.cursor),
29
+ event: row.event,
30
+ data: JSON.parse(row.data),
31
+ recordedAt: row.recorded_at,
32
+ };
33
+ }
34
+ /** Resolve an existing row without writes; legacy retries do not serialize the new payload. */
35
+ export function existingAppendResult({ row, input }, options = {}) {
36
+ if (options.detectConflicts && (row.event !== input.event || row.data !== JSON.stringify(input.data ?? null))) {
37
+ return { kind: 'conflict' };
38
+ }
39
+ return { kind: 'existing', entry: rowToEntry(row) };
40
+ }
41
+ /** Decide replay status before loading payloads, keeping a replay gap to indexed lookups only. */
42
+ export function replayResult({ run, oldest, afterCursor }) {
43
+ if (afterCursor === null) {
44
+ const truncated = oldest !== undefined && oldest.cursor > 1;
45
+ return { kind: 'ok', entries: [], ...(truncated ? { truncated: true } : {}) };
46
+ }
47
+ const oldestRetainedId = oldest ? oldest.cursor : run.next_cursor;
48
+ if (Number(afterCursor) < oldestRetainedId - 1) {
49
+ return { kind: 'replay-gap', requestedCursor: afterCursor, oldestAvailableCursor: oldest ? String(oldest.cursor) : null };
50
+ }
51
+ return { kind: 'ok', entries: [] };
52
+ }
53
+ /** Owner's DDL for both automatic setup and host-managed migrations; no schema fork. */
54
+ export const SQLITE_EVENT_LOG_SCHEMA = `
55
+ CREATE TABLE IF NOT EXISTS jini_event_log_runs (
56
+ run_id TEXT PRIMARY KEY,
57
+ next_cursor INTEGER NOT NULL
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS jini_event_log_entries (
61
+ run_id TEXT NOT NULL,
62
+ cursor INTEGER NOT NULL,
63
+ event TEXT NOT NULL,
64
+ data TEXT NOT NULL,
65
+ recorded_at INTEGER NOT NULL,
66
+ dedupe_key TEXT,
67
+ PRIMARY KEY (run_id, cursor)
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_jini_event_log_entries_dedupe
71
+ ON jini_event_log_entries (run_id, dedupe_key)
72
+ WHERE dedupe_key IS NOT NULL;
73
+ `;
74
+ //# sourceMappingURL=sqlite-policy.js.map
@@ -1,4 +1,3 @@
1
- import { type Clock } from '@jini-ai/core/primitives';
2
1
  /**
3
2
  * `createSqliteEventLog` — the durable `EventLog` adapter (extraction-plan §8 task 8,
4
3
  * §2.6: "`@jini-ai/daemon/store/event-log/sqlite` is the default adapter... an adapter conformance suite covers
@@ -23,35 +22,12 @@ import { type Clock } from '@jini-ai/core/primitives';
23
22
  * adapter preserves its existing bounded SQL/schema behavior rather than adding a hidden archive.
24
23
  */
25
24
  import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
26
- import type { EventLog } from '@jini-ai/protocol';
25
+ import { type SqliteEventLogOptions, type SqliteEventLogWithReceipts } from './sqlite-policy.js';
26
+ export { DEFAULT_MAX_ENTRIES_PER_RUN, SQLITE_EVENT_LOG_SCHEMA } from './sqlite-policy.js';
27
+ export type { SqliteEventLog, SqliteEventLogOptions, SqliteEventLogWithReceipts, SqliteEventLogDatabase, SqliteEventLogAppendOptions, SqliteEventLogAppendResult, } from './sqlite-policy.js';
28
+ export { createKernelSqliteEventLog } from './sqlite-kernel.js';
27
29
  /**
28
- * Default per-run retention cap applied when `SqliteEventLogOptions.maxEntriesPerRun` is
29
- * omitted — see that option's doc for why a durable adapter defaults to a bound instead of
30
- * mirroring the in-memory reference adapter's unbounded-by-default behavior.
31
- */
32
- export declare const DEFAULT_MAX_ENTRIES_PER_RUN = 2000;
33
- export interface SqliteEventLogOptions {
34
- /**
35
- * Maximum entries retained per run before the oldest are evicted. Defaults to
36
- * {@link DEFAULT_MAX_ENTRIES_PER_RUN} (2000) when omitted — unlike the in-memory reference
37
- * adapter (`@jini-ai/daemon`'s `createInMemoryEventLog`), where an omitted cap means unbounded,
38
- * a durable on-disk store defaults to a bounded retention window because unbounded persistent
39
- * growth is a real, unattended operational risk (disk exhaustion across long-lived runs) in a
40
- * way an in-process, GC'd, process-lifetime-bounded structure is not. Pass an explicit value
41
- * (larger, smaller, or `0`) to override the default in either direction — the caller then
42
- * owns that tradeoff explicitly. Must be a non-negative safe integer; anything else (negative,
43
- * fractional, `NaN`, `Infinity`) throws at construction time.
44
- */
45
- readonly maxEntriesPerRun?: number;
46
- /** Core Clock for durable recordedAt values; omitted clock preserves system wall time. */
47
- readonly clock?: Clock;
48
- }
49
- /** A `@jini-ai/daemon` `EventLog` backed by a `better-sqlite3` database, plus a `close()` to release the file handle. */
50
- export interface SqliteEventLog extends EventLog {
51
- close(_args: Record<string, never>): Promise<void>;
52
- }
53
- /**
54
- * Borrows the caller's connection and ensures the dedicated event-log schema idempotently.
30
+ * Borrows the caller's connection; ensures schema unless schemaSetup is 'host'.
55
31
  * Closing this adapter never closes a borrowed handle or changes its pragmas.
56
32
  * Use openSqliteEventLog with an explicit file/opener to acquire an owned connection.
57
33
  * @throws Host SQL/setup failures, or a missing borrowed connection.
@@ -59,7 +35,7 @@ export interface SqliteEventLog extends EventLog {
59
35
  */
60
36
  export declare function createSqliteEventLog(input: {
61
37
  db: SqliteDb;
62
- }, options?: SqliteEventLogOptions): SqliteEventLog;
38
+ }, options?: SqliteEventLogOptions): SqliteEventLogWithReceipts;
63
39
  /**
64
40
  * Acquires a connection through the host opener; setup failure closes that connection.
65
41
  * Successful close is idempotent and closes only this owned handle.
@@ -71,5 +47,5 @@ export declare function createSqliteEventLog(input: {
71
47
  export declare function openSqliteEventLog(input: {
72
48
  file: string;
73
49
  open: SqliteSyncOpener;
74
- }, options?: SqliteEventLogOptions): SqliteEventLog;
50
+ }, options?: SqliteEventLogOptions): SqliteEventLogWithReceipts;
75
51
  //# sourceMappingURL=sqlite.d.ts.map
@@ -23,40 +23,11 @@ import { createSystemClock } from '@jini-ai/core/primitives';
23
23
  * adapter preserves its existing bounded SQL/schema behavior rather than adding a hidden archive.
24
24
  */
25
25
  import { openSqliteConnection } from '@jini-ai/db/sqlite';
26
+ import { resolveMaxEntriesPerRun, assertValidReplayCursor, existingAppendResult, replayResult, rowToEntry, SQLITE_EVENT_LOG_SCHEMA, } from './sqlite-policy.js';
27
+ export { DEFAULT_MAX_ENTRIES_PER_RUN, SQLITE_EVENT_LOG_SCHEMA } from './sqlite-policy.js';
28
+ export { createKernelSqliteEventLog } from './sqlite-kernel.js';
26
29
  /**
27
- * Default per-run retention cap applied when `SqliteEventLogOptions.maxEntriesPerRun` is
28
- * omitted — see that option's doc for why a durable adapter defaults to a bound instead of
29
- * mirroring the in-memory reference adapter's unbounded-by-default behavior.
30
- */
31
- export const DEFAULT_MAX_ENTRIES_PER_RUN = 2000;
32
- /** @internal Validates and resolves `options.maxEntriesPerRun`, applying the documented default. */
33
- function resolveMaxEntriesPerRun(value) {
34
- if (value === undefined)
35
- return DEFAULT_MAX_ENTRIES_PER_RUN;
36
- if (!Number.isSafeInteger(value) || value < 0) {
37
- throw new Error(`createSqliteEventLog: options.maxEntriesPerRun must be a non-negative safe integer, got ${value}`);
38
- }
39
- return value;
40
- }
41
- /** @internal Validates a `replay(runId, afterCursor)` cursor. `null` ("from the beginning") is always valid. */
42
- function assertValidReplayCursor(afterCursor) {
43
- if (afterCursor === null)
44
- return;
45
- const n = Number(afterCursor);
46
- if (!Number.isSafeInteger(n) || n < 0) {
47
- throw new Error(`createSqliteEventLog: invalid replay cursor ${JSON.stringify(afterCursor)} — must be a non-negative safe integer string`);
48
- }
49
- }
50
- function rowToEntry(row) {
51
- return {
52
- id: String(row.cursor),
53
- event: row.event,
54
- data: JSON.parse(row.data),
55
- recordedAt: row.recorded_at,
56
- };
57
- }
58
- /**
59
- * Borrows the caller's connection and ensures the dedicated event-log schema idempotently.
30
+ * Borrows the caller's connection; ensures schema unless schemaSetup is 'host'.
60
31
  * Closing this adapter never closes a borrowed handle or changes its pragmas.
61
32
  * Use openSqliteEventLog with an explicit file/opener to acquire an owned connection.
62
33
  * @throws Host SQL/setup failures, or a missing borrowed connection.
@@ -98,30 +69,12 @@ export function openSqliteEventLog(input, options = {}) {
98
69
  throw error;
99
70
  }
100
71
  }
101
- /** Binds the unchanged cursor/dedupe/retention SQL to a borrowed synchronous connection. */
72
+ /** Binds event-log policies to a borrowed synchronous connection and its atomic transactions. */
102
73
  function bindSqliteEventLog(db, options) {
103
74
  const maxEntriesPerRun = resolveMaxEntriesPerRun(options.maxEntriesPerRun);
104
75
  const clock = options.clock ?? createSystemClock();
105
- db.exec(`
106
- CREATE TABLE IF NOT EXISTS jini_event_log_runs (
107
- run_id TEXT PRIMARY KEY,
108
- next_cursor INTEGER NOT NULL
109
- );
110
-
111
- CREATE TABLE IF NOT EXISTS jini_event_log_entries (
112
- run_id TEXT NOT NULL,
113
- cursor INTEGER NOT NULL,
114
- event TEXT NOT NULL,
115
- data TEXT NOT NULL,
116
- recorded_at INTEGER NOT NULL,
117
- dedupe_key TEXT,
118
- PRIMARY KEY (run_id, cursor)
119
- );
120
-
121
- CREATE INDEX IF NOT EXISTS idx_jini_event_log_entries_dedupe
122
- ON jini_event_log_entries (run_id, dedupe_key)
123
- WHERE dedupe_key IS NOT NULL;
124
- `);
76
+ if (options.schemaSetup !== 'host')
77
+ db.exec(SQLITE_EVENT_LOG_SCHEMA);
125
78
  const getRunStmt = db.prepare('SELECT run_id, next_cursor FROM jini_event_log_runs WHERE run_id = ?');
126
79
  const insertRunStmt = db.prepare('INSERT INTO jini_event_log_runs (run_id, next_cursor) VALUES (?, ?)');
127
80
  const updateNextCursorStmt = db.prepare('UPDATE jini_event_log_runs SET next_cursor = ? WHERE run_id = ?');
@@ -144,66 +97,45 @@ function bindSqliteEventLog(db, options) {
144
97
  // returned `Transaction` type is non-generic), so this runs on `unknown` payloads and the
145
98
  // public `append<Payload>` method below casts at the boundary — the same pattern
146
99
  // `createInMemoryEventLog` uses for its dedupe-hit early return.
147
- const appendTxn = db.transaction((input) => {
100
+ const appendTxn = db.transaction((input, options) => {
101
+ if (options.dedupeKey !== undefined) {
102
+ const existing = getByDedupeStmt.get(input.runId, options.dedupeKey);
103
+ if (existing)
104
+ return existingAppendResult({ row: existing, input }, options);
105
+ }
106
+ if (options.retentionPolicy === 'reject' && countEntriesStmt.get(input.runId).count >= maxEntriesPerRun) {
107
+ return { kind: 'capacity-exceeded', maxEntriesPerRun };
108
+ }
148
109
  let runRow = getRunStmt.get(input.runId);
149
110
  if (!runRow) {
150
111
  insertRunStmt.run(input.runId, 1);
151
112
  runRow = { run_id: input.runId, next_cursor: 1 };
152
113
  }
153
- if (input.dedupeKey !== undefined) {
154
- const existing = getByDedupeStmt.get(input.runId, input.dedupeKey);
155
- if (existing) {
156
- return rowToEntry(existing);
157
- }
158
- }
159
114
  const cursor = runRow.next_cursor;
160
115
  const recordedAt = clock.nowMs();
161
- insertEntryStmt.run(input.runId, cursor, input.event, JSON.stringify(input.data ?? null), recordedAt, input.dedupeKey ?? null);
116
+ insertEntryStmt.run(input.runId, cursor, input.event, JSON.stringify(input.data ?? null), recordedAt, options.dedupeKey ?? null);
162
117
  updateNextCursorStmt.run(cursor + 1, input.runId);
163
118
  const { count } = countEntriesStmt.get(input.runId);
164
119
  if (count > maxEntriesPerRun) {
165
120
  evictOldestStmt.run(input.runId, input.runId, count - maxEntriesPerRun);
166
121
  }
167
- return {
168
- id: String(cursor),
169
- event: input.event,
170
- data: input.data,
171
- recordedAt,
172
- };
122
+ return { kind: 'inserted', entry: { id: String(cursor), event: input.event, data: input.data, recordedAt } };
173
123
  });
174
124
  function replaySync(runId, afterCursor) {
175
125
  // Validated up front, independent of whether `runId` is known: a malformed cursor is a
176
126
  // caller/transport bug regardless of run state, so it throws rather than being folded into
177
127
  // (or masked by) an `'unknown-run'`/`'replay-gap'` result.
178
128
  assertValidReplayCursor(afterCursor);
179
- const runRow = getRunStmt.get(runId);
180
- if (!runRow) {
129
+ const run = getRunStmt.get(runId);
130
+ if (!run)
181
131
  return { kind: 'unknown-run' };
182
- }
183
- if (afterCursor === null) {
184
- const oldestRow = selectOldestStmt.get(runId);
185
- const truncated = oldestRow !== undefined && oldestRow.cursor > 1;
186
- return {
187
- kind: 'ok',
188
- entries: selectAllStmt.all(runId).map(rowToEntry),
189
- ...(truncated ? { truncated: true } : {}),
190
- };
191
- }
132
+ const oldest = selectOldestStmt.get(runId);
133
+ const result = replayResult({ run, oldest, afterCursor });
134
+ if (result.kind !== 'ok')
135
+ return result;
192
136
  // Safe: assertValidReplayCursor already proved this parses to a non-negative safe integer.
193
- const afterCursorNum = Number(afterCursor);
194
- const oldestRow = selectOldestStmt.get(runId);
195
- const oldestRetainedId = oldestRow ? oldestRow.cursor : runRow.next_cursor;
196
- if (afterCursorNum < oldestRetainedId - 1) {
197
- return {
198
- kind: 'replay-gap',
199
- requestedCursor: afterCursor,
200
- oldestAvailableCursor: oldestRow ? String(oldestRow.cursor) : null,
201
- };
202
- }
203
- return {
204
- kind: 'ok',
205
- entries: selectAfterStmt.all(runId, afterCursorNum).map(rowToEntry),
206
- };
137
+ const rows = afterCursor === null ? selectAllStmt.all(runId) : selectAfterStmt.all(runId, Number(afterCursor));
138
+ return { ...result, entries: rows.map(rowToEntry) };
207
139
  }
208
140
  const dropTxn = db.transaction((runId) => {
209
141
  deleteRunEntriesStmt.run(runId);
@@ -211,7 +143,12 @@ function bindSqliteEventLog(db, options) {
211
143
  });
212
144
  return {
213
145
  async append(input, options = {}) {
214
- return appendTxn({ ...input, dedupeKey: options.dedupeKey });
146
+ // Forwarding only dedupeKey guarantees an inserted/existing receipt, never a refusal.
147
+ const result = appendTxn(input, options.dedupeKey === undefined ? {} : { dedupeKey: options.dedupeKey });
148
+ return result.entry;
149
+ },
150
+ async appendWithResult(input, options = {}) {
151
+ return appendTxn(input, options);
215
152
  },
216
153
  async replay({ runId, afterCursor }) {
217
154
  return replaySync(runId, afterCursor);
@@ -0,0 +1,27 @@
1
+ import type { ToolExecutor } from "../tool-executor.js";
2
+ import { type IsRegisteredTool } from "./observability.js";
3
+ /** The one `ProductAnalyticsPort` method this decorator calls. */
4
+ export interface ToolCallAnalytics {
5
+ capture(event: {
6
+ name: string;
7
+ userId: string;
8
+ properties?: Readonly<Record<string, string | number | boolean>>;
9
+ }): void;
10
+ }
11
+ export declare const TOOL_CALL_EVENT = "tool_call";
12
+ /**
13
+ * Wraps `inner` so each `execute` emits one {@link TOOL_CALL_EVENT}: `tool_id` (or the fixed
14
+ * unregistered label), `outcome` (the settled status, or `threw`), `failed`, and `error_kind` for a
15
+ * failure, as `userId` = the principal's pseudonymous id. No input, output or error text is sent.
16
+ * @param required The executor and the analytics port slice.
17
+ * @param optional `isRegisteredTool`: the host's registry check (same rule as the observability decorator).
18
+ * @returns A drop-in `ToolExecutor`; results and throws pass through unchanged.
19
+ * @complexity O(1) beyond `inner`'s own cost.
20
+ */
21
+ export declare function withToolCallAnalytics({ inner, analytics }: {
22
+ inner: ToolExecutor;
23
+ analytics: ToolCallAnalytics;
24
+ }, { isRegisteredTool }?: {
25
+ isRegisteredTool?: IsRegisteredTool;
26
+ }): ToolExecutor;
27
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { pseudonymousUserId, toolCallLabel } from "./observability.js";
2
+ export const TOOL_CALL_EVENT = "tool_call";
3
+ /**
4
+ * Wraps `inner` so each `execute` emits one {@link TOOL_CALL_EVENT}: `tool_id` (or the fixed
5
+ * unregistered label), `outcome` (the settled status, or `threw`), `failed`, and `error_kind` for a
6
+ * failure, as `userId` = the principal's pseudonymous id. No input, output or error text is sent.
7
+ * @param required The executor and the analytics port slice.
8
+ * @param optional `isRegisteredTool`: the host's registry check (same rule as the observability decorator).
9
+ * @returns A drop-in `ToolExecutor`; results and throws pass through unchanged.
10
+ * @complexity O(1) beyond `inner`'s own cost.
11
+ */
12
+ export function withToolCallAnalytics({ inner, analytics }, { isRegisteredTool } = {}) {
13
+ const emit = ({ principal, toolId }, properties) => {
14
+ try {
15
+ analytics.capture({ name: TOOL_CALL_EVENT, userId: pseudonymousUserId({ principal }), properties: { tool_id: toolCallLabel({ toolId }, { isRegisteredTool }), ...properties } });
16
+ }
17
+ catch { /* analytics never fails a tool call */ }
18
+ };
19
+ return {
20
+ execute: async ({ principal, run, toolId, input }, { signal, emitSurface } = {}) => {
21
+ let result;
22
+ try {
23
+ result = await inner.execute({ principal, run, toolId, input }, { ...(signal === undefined ? {} : { signal }), ...(emitSurface === undefined ? {} : { emitSurface }) });
24
+ }
25
+ catch (error) {
26
+ emit({ principal, toolId }, { outcome: "threw", failed: true });
27
+ throw error;
28
+ }
29
+ const failed = result.status === "failed" || result.status === "timed-out";
30
+ emit({ principal, toolId }, { outcome: result.status, failed, ...(result.status === "failed" ? { error_kind: result.errorKind ?? "internal" } : {}) });
31
+ return result;
32
+ },
33
+ resumeConfirmation: (args) => inner.resumeConfirmation(args),
34
+ cancel: (args) => inner.cancel(args),
35
+ getAuditRecord: (args) => inner.getAuditRecord(args),
36
+ };
37
+ }
38
+ //# sourceMappingURL=analytics.js.map
@@ -3,4 +3,6 @@ export * from "./recovery.js";
3
3
  export * from "./redaction.js";
4
4
  export * from "./stack.js";
5
5
  export * from "./federated-refusal.js";
6
+ export * from "./observability.js";
7
+ export * from "./analytics.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -3,4 +3,6 @@ export * from "./recovery.js";
3
3
  export * from "./redaction.js";
4
4
  export * from "./stack.js";
5
5
  export * from "./federated-refusal.js";
6
+ export * from "./observability.js";
7
+ export * from "./analytics.js";
6
8
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file Every tool call as one observed operation. The executor is the one chokepoint every
3
+ * delegated, BYOK and visitor tool call already passes through, so instrumenting it here covers
4
+ * them all without touching a single tool handler or route.
5
+ *
6
+ * The port is taken structurally ({@link ToolCallObservability}), not imported: this package does not
7
+ * depend on `@jini-ai/diagnostics`, and `ObservabilityPort` satisfies the slice as-is. That keeps the
8
+ * daemon free of any observability vendor AND of the diagnostics package; the host composes them.
9
+ * A host whose telemetry is off should not wrap at all (diagnostics' `isNoopObservabilityPort`), so
10
+ * a disabled install keeps its exact executor object and pays no wrapper frame.
11
+ */
12
+ import { type Principal } from "@jini-ai/core";
13
+ import type { ToolExecutor } from "../tool-executor.js";
14
+ type Attributes = Readonly<Record<string, string | number>>;
15
+ /** The two `ObservabilityPort` methods this decorator calls; the port itself satisfies it. */
16
+ export interface ToolCallObservability {
17
+ trackOperation(input: {
18
+ name: string;
19
+ attributes?: Attributes;
20
+ }): {
21
+ run<T>(fn: () => T): T;
22
+ end(outcome: {
23
+ outcome: string;
24
+ failed: boolean;
25
+ attributes?: Attributes;
26
+ }): void;
27
+ };
28
+ reportError(report: {
29
+ error: unknown;
30
+ context: {
31
+ operation: string;
32
+ attributes?: Attributes;
33
+ };
34
+ }): void;
35
+ }
36
+ export declare const TOOL_CALL_OPERATION = "tool.call";
37
+ /** The settled result's text as an Error, so an error tracker gets a type to group on. The text is
38
+ * whatever the inner stack left (the host's `withRedactedToolFailures` has already blanked secrets);
39
+ * adapters redact again regardless. */
40
+ export declare class ToolExecutionFailure extends Error {
41
+ readonly name = "ToolExecutionFailure";
42
+ }
43
+ /** The `tool.id` label for an id nobody registered. Observation starts before the registry has looked
44
+ * the id up, and a delegated request can carry any string there, a pasted key included (review
45
+ * 2026-10-25 #1): an unregistered id is never exported, only this fixed label. */
46
+ export declare const UNREGISTERED_TOOL_LABEL = "unregistered";
47
+ /** Host check, when it has one: is this id a tool the registry knows? */
48
+ export type IsRegisteredTool = (input: {
49
+ toolId: string;
50
+ }) => boolean;
51
+ export declare function toolCallLabel({ toolId }: {
52
+ toolId: string;
53
+ }, { isRegisteredTool }: {
54
+ isRegisteredTool?: IsRegisteredTool | undefined;
55
+ }): string;
56
+ /** WHO did it, as the principal's opaque id (owner 2026-10-25). Pseudonymous only: an id that looks
57
+ * like an e-mail address or carries a secret shape is replaced, never exported. */
58
+ export declare const UNIDENTIFIED_USER_LABEL = "unidentified";
59
+ export declare function pseudonymousUserId({ principal }: {
60
+ principal: Principal;
61
+ }): string;
62
+ /**
63
+ * Wraps `inner` so each `execute` is one `tool.call` operation: `tool.id` (or the fixed
64
+ * {@link UNREGISTERED_TOOL_LABEL}), `agent.run.id` and `user.id` (the principal, pseudonymous) at start,
65
+ * the settled status as outcome, `tool.error_kind` (`validation` vs `internal`) for a failure, and
66
+ * the duration the adapter measures between start and end. Execution errors (an internal failure, a
67
+ * timeout, a non-input throw) also go through `reportError`, inside the operation's scope so trace
68
+ * adapters attach them to its span. A validation failure is NOT reported: it is the caller (usually
69
+ * the model) sending bad input — counted on the operation, but not an incident to page anyone for.
70
+ * @param required The executor and the observability port slice.
71
+ * @param optional `isRegisteredTool`: the host's registry check; without it only the id's shape is checked.
72
+ * @returns A drop-in `ToolExecutor`; results and throws pass through unchanged; other methods delegate.
73
+ * @complexity O(1) beyond `inner`'s own cost.
74
+ */
75
+ export declare function withToolCallObservability({ inner, observability }: {
76
+ inner: ToolExecutor;
77
+ observability: ToolCallObservability;
78
+ }, { isRegisteredTool }?: {
79
+ isRegisteredTool?: IsRegisteredTool;
80
+ }): ToolExecutor;
81
+ export {};
82
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @file Every tool call as one observed operation. The executor is the one chokepoint every
3
+ * delegated, BYOK and visitor tool call already passes through, so instrumenting it here covers
4
+ * them all without touching a single tool handler or route.
5
+ *
6
+ * The port is taken structurally ({@link ToolCallObservability}), not imported: this package does not
7
+ * depend on `@jini-ai/diagnostics`, and `ObservabilityPort` satisfies the slice as-is. That keeps the
8
+ * daemon free of any observability vendor AND of the diagnostics package; the host composes them.
9
+ * A host whose telemetry is off should not wrap at all (diagnostics' `isNoopObservabilityPort`), so
10
+ * a disabled install keeps its exact executor object and pays no wrapper frame.
11
+ */
12
+ import { redactSecrets, ToolInputError } from "@jini-ai/core";
13
+ export const TOOL_CALL_OPERATION = "tool.call";
14
+ // A denial, a declined confirmation and a cancellation are the system working as designed, not
15
+ // failures; a timeout and a failed handler are.
16
+ const FAILED_STATUSES = new Set(["failed", "timed-out"]);
17
+ /** The settled result's text as an Error, so an error tracker gets a type to group on. The text is
18
+ * whatever the inner stack left (the host's `withRedactedToolFailures` has already blanked secrets);
19
+ * adapters redact again regardless. */
20
+ export class ToolExecutionFailure extends Error {
21
+ name = "ToolExecutionFailure";
22
+ }
23
+ /** The `tool.id` label for an id nobody registered. Observation starts before the registry has looked
24
+ * the id up, and a delegated request can carry any string there, a pasted key included (review
25
+ * 2026-10-25 #1): an unregistered id is never exported, only this fixed label. */
26
+ export const UNREGISTERED_TOOL_LABEL = "unregistered";
27
+ /** Registered tool ids are short dotted/underscored names. Used only when the host gives no registry check. */
28
+ const TOOL_ID_SHAPE = /^[A-Za-z][\w.:-]{0,127}$/;
29
+ export function toolCallLabel({ toolId }, { isRegisteredTool }) {
30
+ const shapeOk = TOOL_ID_SHAPE.test(toolId) && redactSecrets({ input: toolId }) === toolId;
31
+ return shapeOk && (isRegisteredTool?.({ toolId }) ?? true) ? toolId : UNREGISTERED_TOOL_LABEL;
32
+ }
33
+ /** WHO did it, as the principal's opaque id (owner 2026-10-25). Pseudonymous only: an id that looks
34
+ * like an e-mail address or carries a secret shape is replaced, never exported. */
35
+ export const UNIDENTIFIED_USER_LABEL = "unidentified";
36
+ export function pseudonymousUserId({ principal }) {
37
+ const id = principal.id;
38
+ if (!id || id.includes("@") || id.length > 128 || redactSecrets({ input: id }) !== id)
39
+ return UNIDENTIFIED_USER_LABEL;
40
+ return id;
41
+ }
42
+ function errorKindOf(result) {
43
+ return result.status === "failed" ? result.errorKind ?? "internal" : undefined;
44
+ }
45
+ /**
46
+ * Wraps `inner` so each `execute` is one `tool.call` operation: `tool.id` (or the fixed
47
+ * {@link UNREGISTERED_TOOL_LABEL}), `agent.run.id` and `user.id` (the principal, pseudonymous) at start,
48
+ * the settled status as outcome, `tool.error_kind` (`validation` vs `internal`) for a failure, and
49
+ * the duration the adapter measures between start and end. Execution errors (an internal failure, a
50
+ * timeout, a non-input throw) also go through `reportError`, inside the operation's scope so trace
51
+ * adapters attach them to its span. A validation failure is NOT reported: it is the caller (usually
52
+ * the model) sending bad input — counted on the operation, but not an incident to page anyone for.
53
+ * @param required The executor and the observability port slice.
54
+ * @param optional `isRegisteredTool`: the host's registry check; without it only the id's shape is checked.
55
+ * @returns A drop-in `ToolExecutor`; results and throws pass through unchanged; other methods delegate.
56
+ * @complexity O(1) beyond `inner`'s own cost.
57
+ */
58
+ export function withToolCallObservability({ inner, observability }, { isRegisteredTool } = {}) {
59
+ return {
60
+ execute: async ({ principal, run, toolId, input }, { signal, emitSurface } = {}) => {
61
+ const attributes = { "tool.id": toolCallLabel({ toolId }, { isRegisteredTool }), "agent.run.id": run.id, "user.id": pseudonymousUserId({ principal }) };
62
+ const context = { operation: TOOL_CALL_OPERATION, attributes };
63
+ const tracker = observability.trackOperation({ name: TOOL_CALL_OPERATION, attributes });
64
+ let result;
65
+ try {
66
+ result = await tracker.run(() => inner.execute({ principal, run, toolId, input }, { ...(signal === undefined ? {} : { signal }), ...(emitSurface === undefined ? {} : { emitSurface }) }));
67
+ }
68
+ catch (error) {
69
+ // An unknown tool id throws ToolInputError: the same caller mistake as a validation failure.
70
+ const errorKind = error instanceof ToolInputError ? "validation" : "internal";
71
+ if (errorKind === "internal")
72
+ tracker.run(() => observability.reportError({ error, context }));
73
+ tracker.end({ outcome: "threw", failed: true, attributes: { "tool.error_kind": errorKind } });
74
+ throw error;
75
+ }
76
+ const errorKind = errorKindOf(result);
77
+ if (result.status === "timed-out" || errorKind === "internal") {
78
+ tracker.run(() => observability.reportError({ error: new ToolExecutionFailure(result.error ?? result.status), context }));
79
+ }
80
+ tracker.end({ outcome: result.status, failed: FAILED_STATUSES.has(result.status), ...(errorKind ? { attributes: { "tool.error_kind": errorKind } } : {}) });
81
+ return result;
82
+ },
83
+ resumeConfirmation: (args) => inner.resumeConfirmation(args),
84
+ cancel: (args) => inner.cancel(args),
85
+ getAuditRecord: (args) => inner.getAuditRecord(args),
86
+ };
87
+ }
88
+ //# sourceMappingURL=observability.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.12",
3
+ "version": "0.6.15-rc.1",
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": {
@@ -139,29 +139,30 @@
139
139
  "@types/pg": "8.23.1",
140
140
  "express": "^4.21.0",
141
141
  "@types/express": "^4.17.21",
142
- "@jini-ai/http-kit": "0.6.12",
143
- "@jini-ai/db": "0.6.12",
144
- "@jini-ai/chat": "0.6.12",
145
- "@jini-ai/agent-runtime": "^0.6.12",
146
- "@jini-ai/platform": "^0.6.12",
147
- "@jini-ai/protocol": "^0.6.12",
148
- "@jini-ai/core": "^0.6.12"
142
+ "@jini-ai/db": "0.6.15-rc.1",
143
+ "@jini-ai/chat": "0.6.15-rc.1",
144
+ "@jini-ai/http-kit": "0.6.15-rc.1",
145
+ "@jini-ai/protocol": "^0.6.15-rc.1",
146
+ "@jini-ai/agent-runtime": "^0.6.15-rc.1",
147
+ "@jini-ai/platform": "^0.6.15-rc.1",
148
+ "@jini-ai/core": "^0.6.15-rc.1"
149
149
  },
150
150
  "dependencies": {
151
- "@jini-ai/protocol": "^0.6.12",
152
- "@jini-ai/core": "^0.6.12"
151
+ "smol-toml": "1.9.1",
152
+ "@jini-ai/core": "^0.6.15-rc.1",
153
+ "@jini-ai/protocol": "^0.6.15-rc.1"
153
154
  },
154
155
  "peerDependencies": {
155
156
  "node-pty": "^1.1.0",
156
- "@jini-ai/db": "^0.6.10",
157
+ "@jini-ai/db": "^0.6.15-rc.1",
157
158
  "kysely": "^0.29.6",
158
159
  "better-sqlite3": "^13.0.0",
159
160
  "@electric-sql/pglite": "0.5.8",
160
161
  "pg": "^8.23.0",
161
- "@jini-ai/http-kit": "^0.6.10",
162
+ "@jini-ai/http-kit": "^0.6.15-rc.1",
162
163
  "express": "^4.21.0 || ^5.0.0",
163
- "@jini-ai/agent-runtime": "^0.6.10",
164
- "@jini-ai/platform": "^0.6.10"
164
+ "@jini-ai/agent-runtime": "^0.6.15-rc.1",
165
+ "@jini-ai/platform": "^0.6.15-rc.1"
165
166
  },
166
167
  "peerDependenciesMeta": {
167
168
  "node-pty": {