@jini-ai/daemon 0.6.15-rc.1 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-executor/codex/home-config.js +3 -2
- package/dist/agent-executor/codex/home-staging.js +2 -2
- package/dist/agent-executor/codex/isolation.js +6 -4
- package/dist/agent-executor/event-translation.js +2 -2
- package/dist/agent-executor/events.js +68 -49
- package/dist/agent-executor/launch.js +39 -50
- package/dist/agent-executor/live-session-state.js +6 -3
- package/dist/agent-executor/mcp-bridge.js +13 -5
- package/dist/agent-executor/message-relay.js +5 -6
- package/dist/agent-executor/run-output.js +7 -2
- package/dist/close-status.js +6 -2
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/read-only-tools.js +5 -1
- package/dist/core/tool-gaps.js +1 -1
- package/dist/delegated-result-correlation.js +2 -2
- package/dist/http/agents.d.ts +4 -0
- package/dist/http/attachments.js +13 -12
- package/dist/http/component-catalog.js +1 -1
- package/dist/http/connectors.js +7 -5
- package/dist/http/db-ops.js +5 -0
- package/dist/http/delegated-tools.js +5 -0
- package/dist/http/frontend-sessions.js +7 -5
- package/dist/http/host-tools.js +7 -5
- package/dist/http/memory.js +3 -1
- package/dist/http/message-relay.js +4 -1
- package/dist/http/model-proxy.js +13 -8
- package/dist/http/research-tavily.adapter.js +2 -1
- package/dist/http/routines.js +7 -5
- package/dist/http/terminals.js +5 -0
- package/dist/http/tool-catalog.js +1 -1
- package/dist/http/xai.js +10 -7
- package/dist/legacy-data-migration.d.ts +0 -12
- package/dist/legacy-data-migration.js +21 -13
- package/dist/remote-tool-executor.js +2 -1
- package/dist/routines/schedule.js +2 -1
- package/dist/routines/scheduler.js +6 -7
- package/dist/run-lifecycle.js +2 -1
- package/dist/store/agent-sessions/sql.js +7 -3
- package/dist/store/event-log/sqlite-kernel.js +5 -1
- package/dist/store/event-log/sqlite.js +6 -2
- package/dist/surface-exchanges.js +3 -3
- package/dist/terminal-session.js +4 -5
- package/dist/tool-executor.js +47 -3
- package/dist/tool-recovery/recovery.js +5 -3
- package/dist/tool-recovery/redaction.js +1 -2
- package/dist/tool-recovery/stack.js +5 -1
- package/dist/tool-result-media.js +7 -1
- package/dist/tool-result-surfaces.js +10 -9
- package/package.json +20 -15
|
@@ -133,6 +133,26 @@ function stagePayload(legacyDir, stagingDir, payloadEntries, fs) {
|
|
|
133
133
|
* @param entries - The payload entries to promote.
|
|
134
134
|
* @returns The entries that were placed into `dataDir` before any failure.
|
|
135
135
|
*/
|
|
136
|
+
/** Renames `src` to `dst`, falling back to copy-then-delete (e.g. across devices); a failed copy
|
|
137
|
+
* removes its partial `dst` before rethrowing. */
|
|
138
|
+
function moveEntry(src, dst, fs) {
|
|
139
|
+
try {
|
|
140
|
+
fs.renameSync(src, dst);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
copyThenRemove(src, dst, fs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function copyThenRemove(src, dst, fs) {
|
|
147
|
+
try {
|
|
148
|
+
fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
149
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
catch (cpErr) {
|
|
152
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
153
|
+
throw cpErr;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
136
156
|
export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs = nodeFilesystem } = {}) {
|
|
137
157
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
138
158
|
const promoted = [];
|
|
@@ -140,19 +160,7 @@ export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs
|
|
|
140
160
|
for (const entry of entries) {
|
|
141
161
|
const src = path.join(stagingDir, entry);
|
|
142
162
|
const dst = path.join(dataDir, entry);
|
|
143
|
-
|
|
144
|
-
fs.renameSync(src, dst);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
try {
|
|
148
|
-
fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
149
|
-
fs.rmSync(src, { recursive: true, force: true });
|
|
150
|
-
}
|
|
151
|
-
catch (cpErr) {
|
|
152
|
-
fs.rmSync(dst, { recursive: true, force: true });
|
|
153
|
-
throw cpErr;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
163
|
+
moveEntry(src, dst, fs);
|
|
156
164
|
promoted.push(entry);
|
|
157
165
|
}
|
|
158
166
|
}
|
|
@@ -146,7 +146,8 @@ function readToolStream({ res, settle, fail }, { emitSurface }) {
|
|
|
146
146
|
res.on('data', (chunk) => {
|
|
147
147
|
buffer += chunk;
|
|
148
148
|
const lines = buffer.split('\n');
|
|
149
|
-
|
|
149
|
+
// String.split always produces at least one member, including for an empty chunk.
|
|
150
|
+
buffer = lines.pop();
|
|
150
151
|
lines.forEach(onLine);
|
|
151
152
|
});
|
|
152
153
|
// Once the terminal line is in hand the outcome is known, so a later reset cannot unsettle it.
|
|
@@ -110,7 +110,8 @@ export function tzWallToUtcGapFallback({ timezone, year, month, day, hour, minut
|
|
|
110
110
|
}
|
|
111
111
|
function matchesWallClock(timezone, at, year, month, day, hour, minute) {
|
|
112
112
|
const p = partsInTimezone({ timezone: timezone, atUtc: at });
|
|
113
|
-
|
|
113
|
+
const sameDate = p.year === year && p.month === month && p.day === day;
|
|
114
|
+
return sameDate && p.hour === hour && p.minute === minute;
|
|
114
115
|
}
|
|
115
116
|
/** Minutes east of UTC for `timezone` at instant `at`. e.g. Asia/Shanghai returns 480. */
|
|
116
117
|
function tzOffsetMinutes(timezone, at) {
|
|
@@ -43,6 +43,11 @@ export class ScheduledRunPersistenceError extends Error {
|
|
|
43
43
|
this.name = 'ScheduledRunPersistenceError';
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/** What a failed scheduled run logs: the underlying error's message when there is one. */
|
|
47
|
+
function scheduledRunFailureDetail(error) {
|
|
48
|
+
const cause = error instanceof ScheduledRunPersistenceError ? error.originalError : error;
|
|
49
|
+
return cause instanceof Error ? cause.message : cause;
|
|
50
|
+
}
|
|
46
51
|
function isScheduledRunPersistenceError(error) {
|
|
47
52
|
return error instanceof ScheduledRunPersistenceError;
|
|
48
53
|
}
|
|
@@ -136,13 +141,7 @@ export class RoutineService {
|
|
|
136
141
|
this.rescheduleOne({ routineId: routine.id });
|
|
137
142
|
})
|
|
138
143
|
.catch((error) => {
|
|
139
|
-
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, error
|
|
140
|
-
? error.originalError instanceof Error
|
|
141
|
-
? error.originalError.message
|
|
142
|
-
: error.originalError
|
|
143
|
-
: error instanceof Error
|
|
144
|
-
? error.message
|
|
145
|
-
: error);
|
|
144
|
+
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, scheduledRunFailureDetail(error));
|
|
146
145
|
if (isScheduledRunPersistenceError(error)) {
|
|
147
146
|
this.retryScheduledSlot(routine.id, fireAt);
|
|
148
147
|
}
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -101,7 +101,8 @@ function terminalStateFromEndEntry(entry) {
|
|
|
101
101
|
if (!isRecord(entry.data))
|
|
102
102
|
return { state: 'failed', resumable: false };
|
|
103
103
|
const status = entry.data.status;
|
|
104
|
-
const
|
|
104
|
+
const unsuccessful = status === 'canceled' ? 'cancelled' : 'failed';
|
|
105
|
+
const state = status === 'succeeded' ? 'succeeded' : unsuccessful;
|
|
105
106
|
return { state, resumable: entry.data.resumable === true };
|
|
106
107
|
}
|
|
107
108
|
// ---------------------------------------------------------------------------
|
|
@@ -13,6 +13,12 @@ export function validateSessionKernel({ kernel, dialect, transports }) {
|
|
|
13
13
|
function runSessionQuery(kernel, body) {
|
|
14
14
|
return kernel.run(db => body(db));
|
|
15
15
|
}
|
|
16
|
+
/** The one atomic upsert behind `setSessionId`: insert the pair, or replace its session id and time. */
|
|
17
|
+
function upsertSessionQuery({ conversationId, agentId, sessionId, updatedAt }) {
|
|
18
|
+
return (db) => db.insertInto('assistant_agent_sessions')
|
|
19
|
+
.values({ conversation_id: conversationId, agent_id: agentId, session_id: sessionId, updated_at: updatedAt })
|
|
20
|
+
.onConflict(oc => oc.columns(['conversation_id', 'agent_id']).doUpdateSet({ session_id: sessionId, updated_at: updatedAt })).execute();
|
|
21
|
+
}
|
|
16
22
|
/** Translate opaque driver failures without losing their cause or mutating host state. */
|
|
17
23
|
async function atSessionBoundary(operation) {
|
|
18
24
|
try {
|
|
@@ -43,9 +49,7 @@ export function createSqlAgentSessionStore({ kernel }, { clock = createSystemClo
|
|
|
43
49
|
setSessionId({ conversationId, agentId, sessionId }) {
|
|
44
50
|
return atSessionBoundary(async () => {
|
|
45
51
|
const updatedAt = clock.nowMs();
|
|
46
|
-
await runSessionQuery(kernel,
|
|
47
|
-
.values({ conversation_id: conversationId, agent_id: agentId, session_id: sessionId, updated_at: updatedAt })
|
|
48
|
-
.onConflict(oc => oc.columns(['conversation_id', 'agent_id']).doUpdateSet({ session_id: sessionId, updated_at: updatedAt })).execute());
|
|
52
|
+
await runSessionQuery(kernel, upsertSessionQuery({ conversationId, agentId, sessionId, updatedAt }));
|
|
49
53
|
});
|
|
50
54
|
},
|
|
51
55
|
clearSessionId({ conversationId, agentId }) {
|
|
@@ -129,7 +129,11 @@ async function replayInKernel({ db, runId, afterCursor }) {
|
|
|
129
129
|
const result = replayResult({ run: { run_id: runId, next_cursor: first.next_cursor }, oldest, afterCursor });
|
|
130
130
|
if (result.kind !== 'ok')
|
|
131
131
|
return result;
|
|
132
|
-
|
|
132
|
+
// A row missing any event column carries no entry.
|
|
133
|
+
const entries = rows.flatMap(({ cursor, event, data, recorded_at }) => {
|
|
134
|
+
const missing = cursor === null || event === null;
|
|
135
|
+
return missing || data === null || recorded_at === null ? [] : [rowToEntry({ cursor, event, data, recorded_at })];
|
|
136
|
+
});
|
|
133
137
|
return { ...result, entries };
|
|
134
138
|
}
|
|
135
139
|
//# sourceMappingURL=sqlite-kernel.js.map
|
|
@@ -58,8 +58,12 @@ export function openSqliteEventLog(input, options = {}) {
|
|
|
58
58
|
} }, { pragmas: ['journal_mode = WAL'] });
|
|
59
59
|
const log = bindSqliteEventLog(db, options);
|
|
60
60
|
let closed = false;
|
|
61
|
-
return { ...log, async close(_args) {
|
|
62
|
-
|
|
61
|
+
return { ...log, async close(_args) {
|
|
62
|
+
if (closed)
|
|
63
|
+
return;
|
|
64
|
+
closed = true;
|
|
65
|
+
db.close();
|
|
66
|
+
} };
|
|
63
67
|
}
|
|
64
68
|
catch (error) {
|
|
65
69
|
try {
|
|
@@ -143,9 +143,9 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
143
143
|
// Unknown and closed are the same situation to a human whose dialog is no longer waiting.
|
|
144
144
|
if (!entry)
|
|
145
145
|
return { ok: false, reason: 'unknown-or-closed' };
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
const toolMismatch = spec.toolId !== undefined && entry.binding.toolId !== spec.toolId;
|
|
147
|
+
const channelMismatch = (entry.binding.channel ?? deps.defaultChannel) !== (spec.channel ?? deps.defaultChannel);
|
|
148
|
+
if (toolMismatch || channelMismatch || entry.binding.principalId !== spec.principalId) {
|
|
149
149
|
// A wrong-binding delivery must not consume a conversation the right human is still having.
|
|
150
150
|
return { ok: false, reason: 'binding-mismatch' };
|
|
151
151
|
}
|
package/dist/terminal-session.js
CHANGED
|
@@ -200,11 +200,10 @@ export function createTerminalSessionManager(_requiredArgs, options = {}) {
|
|
|
200
200
|
const result = [];
|
|
201
201
|
for (const session of terminalService.list()) {
|
|
202
202
|
const meta = metadata.get(session.id);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
result.push(toSessionInfo(session, meta.resourceRef));
|
|
203
|
+
const visible = meta !== undefined && meta.principalId === principal.id
|
|
204
|
+
&& (filter.resourceRef === undefined || meta.resourceRef === filter.resourceRef);
|
|
205
|
+
if (visible)
|
|
206
|
+
result.push(toSessionInfo(session, meta.resourceRef));
|
|
208
207
|
}
|
|
209
208
|
return result;
|
|
210
209
|
}
|
package/dist/tool-executor.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* answer gets back in.
|
|
47
47
|
*/
|
|
48
48
|
import { randomUUID } from 'node:crypto';
|
|
49
|
-
import { ToolInputError, withNormalizationNotes, ToolInputNormalizationError, } from '@jini-ai/core';
|
|
49
|
+
import { ToolInputError, 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
|
/**
|
|
@@ -104,6 +104,8 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
104
104
|
// Finished executionIds, oldest first (Set insertion order): the only records `retireAudit` evicts.
|
|
105
105
|
const finishedAudits = new Set();
|
|
106
106
|
const pendingConfirmations = new Map();
|
|
107
|
+
// Owned by the executor, not a bridge: concurrent HTTP/MCP calls create separate bridges.
|
|
108
|
+
const approvalWaits = new Map();
|
|
107
109
|
const activeControllers = new Map();
|
|
108
110
|
/**
|
|
109
111
|
* Marks a pending confirmation `cancel()` just resolved (as opposed to a genuine human `deny`),
|
|
@@ -114,6 +116,34 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
114
116
|
* `requestConfirmation()` settles, so it can never leak onto a later execution.
|
|
115
117
|
*/
|
|
116
118
|
const cancelledConfirmations = new Set();
|
|
119
|
+
/** Records only actual human waits; terminal decisions and aborts release the run's write block.
|
|
120
|
+
* @complexity O(1) time/space per pending execution; empty run entries are removed. */
|
|
121
|
+
function approvalStateChange({ run, executionId, toolId, pending }, _optional = {}) {
|
|
122
|
+
if (pending) {
|
|
123
|
+
const waits = approvalWaits.get(run.id) ?? new Map();
|
|
124
|
+
waits.set(executionId, toolId);
|
|
125
|
+
approvalWaits.set(run.id, waits);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const waits = approvalWaits.get(run.id);
|
|
129
|
+
waits?.delete(executionId);
|
|
130
|
+
if (waits?.size === 0)
|
|
131
|
+
approvalWaits.delete(run.id);
|
|
132
|
+
}
|
|
133
|
+
/** Refuses writes using the registered read-only contract, never a model's claim.
|
|
134
|
+
* The validation result reaches every delegated transport as an actionable error, without
|
|
135
|
+
* resolving or approving the original call. @complexity O(1) time/space. */
|
|
136
|
+
function approvalRefusal({ executionId, run, descriptor }, _optional = {}) {
|
|
137
|
+
const pendingToolId = approvalWaits.get(run.id)?.values().next().value;
|
|
138
|
+
if (pendingToolId === undefined || isReadOnlyTool({ descriptor }))
|
|
139
|
+
return undefined;
|
|
140
|
+
const error = JSON.stringify({
|
|
141
|
+
status: 'awaiting_approval', started: false, toolId: descriptor.id, pendingToolId,
|
|
142
|
+
message: 'Another action is waiting for human confirmation and has not started. This write was refused. Reads may continue. Never say an action has started until its tool result explicitly says it started.',
|
|
143
|
+
});
|
|
144
|
+
appendEvent(executionId, 'failed', error);
|
|
145
|
+
return { executionId, status: 'failed', errorKind: 'validation', error };
|
|
146
|
+
}
|
|
117
147
|
function appendEvent(executionId, phase, detail) {
|
|
118
148
|
// Every call site passes an `executionId` this same `execute()` call
|
|
119
149
|
// already registered in `audits` (right before its first `appendEvent`
|
|
@@ -242,7 +272,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
242
272
|
* Acts on the authorization outcome synchronously: `{ refusal }` if the call ends here
|
|
243
273
|
* (cancelled meanwhile, or denied), otherwise records `authorized` and returns `{ allowed }`.
|
|
244
274
|
*/
|
|
245
|
-
function admitAuthorized({ executionId, resolved, signal }) {
|
|
275
|
+
function admitAuthorized({ executionId, resolved, signal, run }) {
|
|
246
276
|
// `authorize()` isn't itself signal-aware — it's typically a fast policy check, not a
|
|
247
277
|
// human-in-the-loop wait — so this is a post-await check rather than a mid-flight abort, the
|
|
248
278
|
// same idiom the post-handler check below already uses: a signal that fired while `authorize()`
|
|
@@ -253,6 +283,10 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
253
283
|
return { refusal: terminal({ executionId, status: 'denied' }) };
|
|
254
284
|
const { normalizationNotes } = resolved;
|
|
255
285
|
appendEvent(executionId, 'authorized', normalizationNotes.length ? `normalized: ${normalizationNotes.join('; ')}` : undefined);
|
|
286
|
+
// Check after authorization awaits: an approval may have opened while this policy was pending.
|
|
287
|
+
const refusal = approvalRefusal({ executionId, run, descriptor: resolved.descriptor });
|
|
288
|
+
if (refusal)
|
|
289
|
+
return { refusal };
|
|
256
290
|
return { allowed: resolved };
|
|
257
291
|
}
|
|
258
292
|
/**
|
|
@@ -268,13 +302,18 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
268
302
|
async function confirmGate(request) {
|
|
269
303
|
const { executionId } = request;
|
|
270
304
|
let confirmation;
|
|
305
|
+
const state = { run: request.run, executionId, toolId: request.tool.id };
|
|
271
306
|
try {
|
|
307
|
+
approvalStateChange({ ...state, pending: true });
|
|
272
308
|
confirmation = await requestConfirmation(request);
|
|
273
309
|
}
|
|
274
310
|
catch {
|
|
275
311
|
// Same reasoning as the authorization gate: terminal, recorded, and generic.
|
|
276
312
|
return operationalFailure({ executionId, error: 'confirmation failed' });
|
|
277
313
|
}
|
|
314
|
+
finally {
|
|
315
|
+
approvalStateChange({ ...state, pending: false });
|
|
316
|
+
}
|
|
278
317
|
if (confirmation === 'confirm') {
|
|
279
318
|
appendEvent(executionId, 'confirmed');
|
|
280
319
|
return undefined;
|
|
@@ -353,6 +392,10 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
353
392
|
if (controller.signal.aborted) {
|
|
354
393
|
return terminal({ executionId, status: 'cancelled' });
|
|
355
394
|
}
|
|
395
|
+
// Guidance may await I/O after admission; another call can open a confirmation in that gap.
|
|
396
|
+
const refusal = approvalRefusal({ executionId, run, descriptor });
|
|
397
|
+
if (refusal)
|
|
398
|
+
return refusal;
|
|
356
399
|
const timeout = startTimeout(controller, descriptor.timeoutMs);
|
|
357
400
|
// Cleanup (clear the timeout) is repeated at
|
|
358
401
|
// each return below rather than centralized in a `finally` — a
|
|
@@ -379,6 +422,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
379
422
|
signal: controller.signal,
|
|
380
423
|
}, {
|
|
381
424
|
...surfaceOption(emitSurface),
|
|
425
|
+
onApprovalStateChange: ({ pending }) => approvalStateChange({ run, executionId, toolId: call.toolId, pending }),
|
|
382
426
|
invokeTool: nestedInvoker({ principal, run, toolId: call.toolId, invocationRegistry: call.invocationRegistry, signal: controller.signal }, { emitSurface }),
|
|
383
427
|
});
|
|
384
428
|
cleanup();
|
|
@@ -443,7 +487,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
443
487
|
// `invocationRegistry.has({ toolId })` above already confirmed the tool exists, and `ToolRegistry` is
|
|
444
488
|
// append-only (no unregister — see `createToolRegistry`'s doc), so `authorizeToolInvocation`
|
|
445
489
|
// resolving the same `toolId` against the same `registry` cannot come back `undefined` here.
|
|
446
|
-
const gate = admitAuthorized({ executionId, resolved: resolved, signal: controller.signal });
|
|
490
|
+
const gate = admitAuthorized({ executionId, resolved: resolved, signal: controller.signal, run });
|
|
447
491
|
if ('refusal' in gate) {
|
|
448
492
|
cleanupCancellation();
|
|
449
493
|
return gate.refusal;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isIssuedToolFailureDiagnostic } from "./diagnostics.js";
|
|
2
|
-
import { askOnce, SURFACE_DISMISSED_PARAM,
|
|
2
|
+
import { askOnce, SURFACE_DISMISSED_PARAM, } from "../surface-exchanges.js";
|
|
3
3
|
import { refuseNonReadOnlyDispatch } from "../core/read-only-tools.js";
|
|
4
4
|
/**
|
|
5
5
|
* @file The CONSUMER of `contracts/core/tool-failure-diagnostics.ts`'s `ToolFailureDiagnostic`
|
|
@@ -119,7 +119,9 @@ function isActionableDiagnostic(value) {
|
|
|
119
119
|
* @complexity O(1).
|
|
120
120
|
*/
|
|
121
121
|
function remedyReportedFailure(output) {
|
|
122
|
-
|
|
122
|
+
if (!isPlainObject(output))
|
|
123
|
+
return false;
|
|
124
|
+
return output["saved"] === false || output["created"] === false || output["cancelled"] === true || output["connected"] === false;
|
|
123
125
|
}
|
|
124
126
|
/** `true` once the bounded walk should stop descending — depth past {@link MAX_DIAGNOSTIC_SCAN_DEPTH}
|
|
125
127
|
* or the node budget exhausted. Pulled out of {@link walkForDiagnostic} so that function's own guard
|
|
@@ -400,7 +402,7 @@ function remedyApplied(remedyResult) {
|
|
|
400
402
|
export function withToolFailureRecovery({ inner, ...deps }, _optional = {}) {
|
|
401
403
|
return {
|
|
402
404
|
execute: async (required, optional = {}) => {
|
|
403
|
-
const { principal,
|
|
405
|
+
const { principal, input } = required;
|
|
404
406
|
const { signal, emitSurface } = optional;
|
|
405
407
|
const result = await inner.execute(required, optional);
|
|
406
408
|
// SILENCE STAYS FREE: everything but a completed call whose output carries an actionable
|
|
@@ -148,8 +148,7 @@ function redactFailedResult(result, context, deps) {
|
|
|
148
148
|
return result;
|
|
149
149
|
return { ...result, error: deps.redactSecretShapes({ text: result.error }).text };
|
|
150
150
|
}
|
|
151
|
-
const mintErrorId = deps
|
|
152
|
-
const onFailure = deps.onFailure;
|
|
151
|
+
const { mintErrorId, onFailure } = deps;
|
|
153
152
|
const { text, redactions } = deps.redactSecretShapes({ text: result.error ?? `tool "${context.toolId}" failed` });
|
|
154
153
|
const errorId = mintErrorId();
|
|
155
154
|
safeOnFailure(onFailure, {
|
|
@@ -46,6 +46,10 @@ import { withReadOnlyToolConstraint } from "../core/read-only-tools.js";
|
|
|
46
46
|
import { withToolAttemptAudit } from "../core/tool-audit.js";
|
|
47
47
|
import { withToolFailureRecovery } from "./recovery.js";
|
|
48
48
|
import { withRedactedToolFailures, readToolErrorId } from "./redaction.js";
|
|
49
|
+
/** The audit wrapper's options: only an error sink the caller actually supplied. */
|
|
50
|
+
function auditOptions(audit) {
|
|
51
|
+
return audit.onSinkError ? { onSinkError: audit.onSinkError } : {};
|
|
52
|
+
}
|
|
49
53
|
/** One registry and innermost read-only gate protect original, remedy and retry alike.
|
|
50
54
|
* Redaction precedes observation; recovery is outermost so each actual attempt is audited.
|
|
51
55
|
* @complexity O(1) composition; each decorator owns its runtime cost.
|
|
@@ -55,7 +59,7 @@ export function createAssistantToolExecutor(deps, _optional = {}) {
|
|
|
55
59
|
registry: deps.registry, idGenerator: { newId: deps.newExecutionId }, messages: deps.readOnlyMessages }, {});
|
|
56
60
|
const redacted = withRedactedToolFailures({ inner: guarded, ...deps.toolFailures }, {});
|
|
57
61
|
const audited = deps.toolAttemptAudit
|
|
58
|
-
? withToolAttemptAudit({ inner: redacted, ...deps.toolAttemptAudit, readErrorId: readToolErrorId }, deps.toolAttemptAudit
|
|
62
|
+
? withToolAttemptAudit({ inner: redacted, ...deps.toolAttemptAudit, readErrorId: readToolErrorId }, auditOptions(deps.toolAttemptAudit))
|
|
59
63
|
: redacted;
|
|
60
64
|
return withToolFailureRecovery({ ...deps, inner: audited }, {});
|
|
61
65
|
}
|
|
@@ -74,6 +74,11 @@ export function resultContent({ result }) {
|
|
|
74
74
|
return 'Tool execution cancelled.';
|
|
75
75
|
case 'failed':
|
|
76
76
|
return result.error ?? 'Tool execution failed.';
|
|
77
|
+
default: {
|
|
78
|
+
// Unreachable: every tool result status has a case above, and a new one fails typecheck here.
|
|
79
|
+
const unhandled = result.status;
|
|
80
|
+
throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
|
|
81
|
+
}
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
/** Projects a model-safe result into the saved event: pixels occur only in `media`.
|
|
@@ -81,9 +86,10 @@ export function resultContent({ result }) {
|
|
|
81
86
|
* @complexity O(n) output bytes, with O(n) transient serialization space. */
|
|
82
87
|
export function projectToolResultEvent({ result }, _options = {}) {
|
|
83
88
|
const { remainder, media } = extractResultMedia({ output: result.output });
|
|
89
|
+
const outputFlagsError = isRecord(result.output) && result.output.isError === true;
|
|
84
90
|
const isError = result.status !== 'completed'
|
|
85
91
|
|| (batchOutcomeFor({ output: result.output }) ?? 'completed') !== 'completed'
|
|
86
|
-
||
|
|
92
|
+
|| outputFlagsError;
|
|
87
93
|
return {
|
|
88
94
|
content: resultContent({ result: { ...result, output: remainder } }),
|
|
89
95
|
...(isError ? { isError: true } : {}),
|
|
@@ -52,6 +52,13 @@
|
|
|
52
52
|
* break the feature. Anything else absent from this list is still treated as human-only.
|
|
53
53
|
*/
|
|
54
54
|
export const MODEL_VISIBLE_BLOCK_TYPES = ['text', 'image'];
|
|
55
|
+
/** A record block whose string `type` is one the model may see. */
|
|
56
|
+
function isModelVisibleBlock(block) {
|
|
57
|
+
if (!isRecord(block))
|
|
58
|
+
return false;
|
|
59
|
+
const type = block['type'];
|
|
60
|
+
return typeof type === 'string' && MODEL_VISIBLE_BLOCK_TYPES.includes(type);
|
|
61
|
+
}
|
|
55
62
|
function isRecord(value) {
|
|
56
63
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
64
|
}
|
|
@@ -74,16 +81,10 @@ export function splitToolResultSurfaces({ output }) {
|
|
|
74
81
|
for (const block of content) {
|
|
75
82
|
// A non-record block carries no `type` to check, so it cannot be shown to be model-safe.
|
|
76
83
|
// Withheld rather than passed through, per this module's fail-closed rule.
|
|
77
|
-
if (
|
|
78
|
-
surfaces.push({ type: 'unknown', value: block });
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const type = block['type'];
|
|
82
|
-
if (typeof type === 'string' && MODEL_VISIBLE_BLOCK_TYPES.includes(type)) {
|
|
84
|
+
if (isModelVisibleBlock(block))
|
|
83
85
|
modelBlocks.push(block);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
surfaces.push(block);
|
|
86
|
+
else
|
|
87
|
+
surfaces.push(isRecord(block) ? block : { type: 'unknown', value: block });
|
|
87
88
|
}
|
|
88
89
|
if (surfaces.length === 0)
|
|
89
90
|
return { modelOutput: output, surfaces: [] };
|
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.15",
|
|
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": {
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
"registry": "https://registry.npmjs.org"
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
|
+
"smol-toml": "1.9.1",
|
|
132
133
|
"@vitest/coverage-v8": "^2.1.9",
|
|
133
134
|
"node-pty": "^1.1.0",
|
|
134
135
|
"better-sqlite3": "^13.0.0",
|
|
@@ -139,32 +140,35 @@
|
|
|
139
140
|
"@types/pg": "8.23.1",
|
|
140
141
|
"express": "^4.21.0",
|
|
141
142
|
"@types/express": "^4.17.21",
|
|
142
|
-
"@jini-ai/db": "0.6.15
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/
|
|
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"
|
|
149
150
|
},
|
|
150
151
|
"dependencies": {
|
|
151
|
-
"
|
|
152
|
-
"@jini-ai/
|
|
153
|
-
"@jini-ai/protocol": "^0.6.15-rc.1"
|
|
152
|
+
"@jini-ai/core": "^0.6.15",
|
|
153
|
+
"@jini-ai/protocol": "^0.6.15"
|
|
154
154
|
},
|
|
155
155
|
"peerDependencies": {
|
|
156
|
+
"smol-toml": "1.9.1",
|
|
156
157
|
"node-pty": "^1.1.0",
|
|
157
|
-
"@jini-ai/db": "^0.6.
|
|
158
|
+
"@jini-ai/db": "^0.6.10",
|
|
158
159
|
"kysely": "^0.29.6",
|
|
159
160
|
"better-sqlite3": "^13.0.0",
|
|
160
161
|
"@electric-sql/pglite": "0.5.8",
|
|
161
162
|
"pg": "^8.23.0",
|
|
162
|
-
"@jini-ai/http-kit": "^0.6.
|
|
163
|
+
"@jini-ai/http-kit": "^0.6.10",
|
|
163
164
|
"express": "^4.21.0 || ^5.0.0",
|
|
164
|
-
"@jini-ai/agent-runtime": "^0.6.
|
|
165
|
-
"@jini-ai/platform": "^0.6.
|
|
165
|
+
"@jini-ai/agent-runtime": "^0.6.10",
|
|
166
|
+
"@jini-ai/platform": "^0.6.10"
|
|
166
167
|
},
|
|
167
168
|
"peerDependenciesMeta": {
|
|
169
|
+
"smol-toml": {
|
|
170
|
+
"optional": true
|
|
171
|
+
},
|
|
168
172
|
"node-pty": {
|
|
169
173
|
"optional": true
|
|
170
174
|
},
|
|
@@ -200,6 +204,7 @@
|
|
|
200
204
|
"build": "tsc -p tsconfig.json",
|
|
201
205
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
202
206
|
"test": "vitest run",
|
|
207
|
+
"test:live:claude-bash": "vitest run --config vitest.claude-bash.live.config.ts",
|
|
203
208
|
"test:coverage": "vitest run --coverage",
|
|
204
209
|
"test:postgres": "vitest run --config vitest.postgres.config.ts src/store/agent-sessions/__tests__/postgres.postgres.test.ts"
|
|
205
210
|
}
|