@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.
- package/CHANGELOG.md +34 -0
- package/dist/agent-executor/codex/home-config.d.ts +50 -0
- package/dist/agent-executor/codex/home-config.js +209 -0
- package/dist/agent-executor/codex/home-staging.d.ts +60 -0
- package/dist/agent-executor/codex/home-staging.js +195 -0
- package/dist/agent-executor/codex/isolation.d.ts +27 -0
- package/dist/agent-executor/codex/isolation.js +67 -0
- package/dist/agent-executor/contracts.d.ts +4 -1
- package/dist/agent-executor/event-translation.d.ts +7 -7
- package/dist/agent-executor/event-translation.js +17 -17
- package/dist/agent-executor/events.js +1 -1
- package/dist/agent-executor/index.d.ts +3 -1
- package/dist/agent-executor/index.js +3 -1
- package/dist/agent-executor/launch.d.ts +3 -2
- package/dist/agent-executor/launch.js +68 -17
- package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
- package/dist/agent-executor/mcp-bridge-probe.js +1 -1
- package/dist/agent-executor/mcp-bridge.d.ts +9 -86
- package/dist/agent-executor/mcp-bridge.js +4 -355
- package/dist/ai-trace-lifecycle.d.ts +48 -0
- package/dist/ai-trace-lifecycle.js +116 -0
- package/dist/core/read-only-tools.js +3 -1
- package/dist/delegated-tool-bridge.js +19 -1
- package/dist/http/attachment-lookup-routes.d.ts +44 -0
- package/dist/http/attachment-lookup-routes.js +71 -0
- package/dist/http/attachments.d.ts +112 -7
- package/dist/http/attachments.js +58 -17
- package/dist/http/connectors.js +23 -18
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +3 -0
- package/dist/http/tool-execution-routes.d.ts +69 -0
- package/dist/http/tool-execution-routes.js +181 -0
- package/dist/http/xai.js +19 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/remote-attachment-lookup.d.ts +29 -0
- package/dist/remote-attachment-lookup.js +71 -0
- package/dist/remote-tool-executor.d.ts +85 -0
- package/dist/remote-tool-executor.js +250 -0
- package/dist/routing-tool-executor.d.ts +29 -0
- package/dist/routing-tool-executor.js +16 -0
- package/dist/run-lifecycle.d.ts +4 -1
- package/dist/run-lifecycle.js +35 -5
- package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
- package/dist/store/event-log/sqlite-kernel.js +135 -0
- package/dist/store/event-log/sqlite-policy.d.ts +93 -0
- package/dist/store/event-log/sqlite-policy.js +74 -0
- package/dist/store/event-log/sqlite.d.ts +7 -31
- package/dist/store/event-log/sqlite.js +32 -95
- package/dist/tool-recovery/analytics.d.ts +27 -0
- package/dist/tool-recovery/analytics.js +38 -0
- package/dist/tool-recovery/index.d.ts +2 -0
- package/dist/tool-recovery/index.js +2 -0
- package/dist/tool-recovery/observability.d.ts +82 -0
- package/dist/tool-recovery/observability.js +88 -0
- 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
|
|
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
|
-
*
|
|
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):
|
|
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):
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
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,
|
|
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
|
|
180
|
-
if (!
|
|
129
|
+
const run = getRunStmt.get(runId);
|
|
130
|
+
if (!run)
|
|
181
131
|
return { kind: 'unknown-run' };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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/
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/core": "^0.6.
|
|
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
|
-
"
|
|
152
|
-
"@jini-ai/core": "^0.6.
|
|
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.
|
|
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.
|
|
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.
|
|
164
|
-
"@jini-ai/platform": "^0.6.
|
|
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": {
|