@jini-ai/daemon 0.5.3 → 0.6.0
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 +4 -0
- package/dist/agent-executor/contracts.d.ts +45 -14
- package/dist/agent-executor/contracts.js +0 -9
- package/dist/agent-executor/event-translation.d.ts +2 -2
- package/dist/agent-executor/event-translation.js +0 -2
- package/dist/agent-executor/events.d.ts +26 -6
- package/dist/agent-executor/events.js +198 -11
- package/dist/agent-executor/handoff.d.ts +1 -1
- package/dist/agent-executor/handoff.js +0 -1
- package/dist/agent-executor/index.d.ts +1 -1
- package/dist/agent-executor/launch.d.ts +1 -1
- package/dist/agent-executor/launch.js +47 -3
- package/dist/agent-executor/mcp-bridge.d.ts +2 -2
- package/dist/agent-executor/mcp-bridge.js +19 -8
- package/dist/agent-executor/model-receipts.d.ts +6 -0
- package/dist/agent-executor/model-receipts.js +26 -0
- package/dist/agent-executor.d.ts +7 -1665
- package/dist/agent-executor.js +7 -3007
- package/dist/core/read-only-tools.d.ts +130 -0
- package/dist/core/read-only-tools.js +115 -0
- package/dist/core/tool-audit.d.ts +131 -0
- package/dist/core/tool-audit.js +113 -0
- package/dist/http/active-context.d.ts +2 -2
- package/dist/http/agents.d.ts +8 -2
- package/dist/http/agents.js +6 -2
- package/dist/http/component-catalog.d.ts +2 -2
- package/dist/http/connectors.d.ts +17 -17
- package/dist/http/daemon-status.d.ts +2 -2
- package/dist/http/db-ops.d.ts +3 -3
- package/dist/http/delegated-tools.d.ts +1 -1
- package/dist/http/delegated-tools.js +1 -1
- package/dist/http/frontend-sessions.d.ts +1 -1
- package/dist/http/health.d.ts +6 -6
- package/dist/http/host-tools.d.ts +2 -2
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.d.ts +4 -4
- package/dist/http/memory.d.ts +15 -15
- package/dist/http/remote-run-events.d.ts +2 -2
- package/dist/http/research.d.ts +1 -1
- package/dist/http/route-manifest.js +2 -2
- package/dist/http/routines.d.ts +7 -7
- package/dist/http/run-credentials.d.ts +2 -2
- package/dist/http/run-credentials.js +2 -2
- package/dist/http/runs.d.ts +31 -6
- package/dist/http/runs.js +36 -1
- package/dist/http/terminals.d.ts +6 -6
- package/dist/http/tool-catalog.d.ts +2 -2
- package/dist/http/xai.d.ts +6 -6
- package/dist/index.d.ts +75 -571
- package/dist/index.js +75 -571
- package/dist/read-only-tools.d.ts +1 -129
- package/dist/read-only-tools.js +2 -114
- package/dist/run-credentials.d.ts +47 -23
- package/dist/run-credentials.js +24 -8
- package/dist/scheduler.d.ts +5 -0
- package/dist/scheduler.js +13 -1
- package/dist/session-coordination.d.ts +58 -24
- package/dist/session-coordination.js +40 -12
- package/dist/store/event-log/sqlite.js +3 -3
- package/dist/surface-exchanges.d.ts +207 -61
- package/dist/surface-exchanges.js +167 -20
- package/dist/tool-audit.d.ts +1 -113
- package/dist/tool-audit.js +2 -94
- package/dist/tool-recovery/diagnostics.d.ts +132 -0
- package/dist/tool-recovery/diagnostics.js +110 -0
- package/dist/tool-recovery/federated-refusal.d.ts +92 -0
- package/dist/tool-recovery/federated-refusal.js +152 -0
- package/dist/tool-recovery/index.d.ts +6 -0
- package/dist/tool-recovery/index.js +6 -0
- package/dist/tool-recovery/recovery.d.ts +49 -0
- package/dist/tool-recovery/recovery.js +419 -0
- package/dist/tool-recovery/redaction.d.ts +166 -0
- package/dist/tool-recovery/redaction.js +192 -0
- package/dist/tool-recovery/stack.d.ts +69 -0
- package/dist/tool-recovery/stack.js +62 -0
- package/package.json +35 -12
|
@@ -1,15 +1,58 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export { SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWER_PARAM, } from '@jini-ai/core';
|
|
2
|
+
/**
|
|
3
|
+
* How long an exchange may sit with nothing happening before it gives up.
|
|
4
|
+
*
|
|
5
|
+
* Per *turn*, not per exchange: it resets whenever a message is sent or received, so a long
|
|
6
|
+
* multi-turn conversation is not punished for taking several turns — only for a human who walked
|
|
7
|
+
* away. Long enough to read a dialog and decide, short enough that an abandoned tab does not hold a
|
|
8
|
+
* live agent subprocess open.
|
|
9
|
+
*/
|
|
4
10
|
export const DEFAULT_SURFACE_IDLE_TTL_MS = 5 * 60 * 1000;
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling on one exchange's total life, regardless of activity.
|
|
13
|
+
*
|
|
14
|
+
* Exists because the idle deadline alone is unbounded in aggregate — a human answering every four
|
|
15
|
+
* minutes could hold a call open indefinitely, and the call is not ours to hold: it is an HTTP
|
|
16
|
+
* request from the spawned agent's MCP server, which gives up after 6 minutes (`@jini-ai/mcp`'s
|
|
17
|
+
* delegated-tool deadline). Sized just inside that, so a stalled exchange returns an explicit result
|
|
18
|
+
* the model can read rather than having the transport time out underneath it.
|
|
19
|
+
*
|
|
20
|
+
* **This, not the idle deadline, is what caps a multi-turn conversation today.** Raising it means
|
|
21
|
+
* raising the transport deadline first; the two must move together or the ordering inverts.
|
|
22
|
+
*/
|
|
5
23
|
export const DEFAULT_SURFACE_MAX_LIFETIME_MS = 5.5 * 60 * 1000;
|
|
6
|
-
/**
|
|
24
|
+
/** Distinguishes an ended exchange from an emitter failure without matching error text. */
|
|
25
|
+
class SurfaceExchangeEndedError extends Error {
|
|
26
|
+
constructor({ id, terminal }) {
|
|
27
|
+
super(`surface exchange ${id} has already ended`);
|
|
28
|
+
this.terminal = terminal;
|
|
29
|
+
}
|
|
30
|
+
terminal;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates an in-process store of open surface exchanges.
|
|
34
|
+
*
|
|
35
|
+
* In-process necessarily, not merely by choice: an open exchange is a suspended function call in
|
|
36
|
+
* this process's memory. It could not be shared across processes even if that were desirable.
|
|
37
|
+
* `mcp-ui-tool-calls-route.ts` is mounted on the same daemon the handlers run in, which is what makes
|
|
38
|
+
* delivery a direct call rather than another network hop.
|
|
39
|
+
*
|
|
40
|
+
* @param options.idleTtlMs - Per-turn inactivity deadline. See {@link DEFAULT_SURFACE_IDLE_TTL_MS}.
|
|
41
|
+
* @param options.maxLifetimeMs - Total lifetime ceiling. See {@link DEFAULT_SURFACE_MAX_LIFETIME_MS}.
|
|
42
|
+
* @param deps.idGenerator - Id source, injected so a test can assert a known value.
|
|
43
|
+
* @param deps.clock - Wall clock for {@link SurfaceExchange.expiresAtMs}. The injected scheduler owns
|
|
44
|
+
* the timers; the clock dates and checks their deadlines.
|
|
45
|
+
* @complexity O(1) per send/receive/deliver. Each exchange owns two `unref`'d timers.
|
|
46
|
+
*/
|
|
7
47
|
export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_IDLE_TTL_MS, maxLifetimeMs = DEFAULT_SURFACE_MAX_LIFETIME_MS } = {}) {
|
|
8
48
|
if (!Number.isFinite(idleTtlMs) || idleTtlMs <= 0 || !Number.isFinite(maxLifetimeMs) || maxLifetimeMs <= 0)
|
|
9
49
|
throw new Error('surface deadlines must be finite and positive');
|
|
10
50
|
const openExchanges = new Map();
|
|
11
51
|
return {
|
|
12
|
-
open({ binding, emit }) {
|
|
52
|
+
open({ binding, emit }, _optional = {}) {
|
|
53
|
+
// Waiting without a live emitter is a deadlock; reject before allocating any timers or ID.
|
|
54
|
+
if (typeof emit !== 'function')
|
|
55
|
+
throw new Error('surface exchange requires an emitter');
|
|
13
56
|
const id = deps.idGenerator.newId();
|
|
14
57
|
if (!id || openExchanges.has(id))
|
|
15
58
|
throw new Error('surface exchange ID must be unique and nonempty');
|
|
@@ -62,20 +105,22 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
62
105
|
const waiter = waiters.shift();
|
|
63
106
|
if (waiter)
|
|
64
107
|
waiter({ status: 'received', params });
|
|
108
|
+
// The human can answer while the handler composes its next send. Queue rather than
|
|
109
|
+
// drop: dropping would deadlock the exchange on an answer already given.
|
|
65
110
|
else
|
|
66
111
|
inbox.push(params);
|
|
67
112
|
},
|
|
68
113
|
});
|
|
69
114
|
return {
|
|
70
115
|
id,
|
|
71
|
-
async send({ emission }) {
|
|
116
|
+
async send({ emission }, _optional = {}) {
|
|
72
117
|
expireIfDue();
|
|
73
118
|
if (terminal)
|
|
74
|
-
throw new
|
|
119
|
+
throw new SurfaceExchangeEndedError({ id, terminal });
|
|
75
120
|
armIdle();
|
|
76
121
|
await emit(emission);
|
|
77
122
|
},
|
|
78
|
-
async receive(_args) {
|
|
123
|
+
async receive(_args, _optional = {}) {
|
|
79
124
|
expireIfDue();
|
|
80
125
|
// Preserve the original FIFO contract: already delivered answers precede terminal status.
|
|
81
126
|
const buffered = inbox.shift();
|
|
@@ -87,26 +132,31 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
87
132
|
return terminal;
|
|
88
133
|
return new Promise(resolve => waiters.push(resolve));
|
|
89
134
|
},
|
|
90
|
-
close(_args) { end({ status: 'abandoned' }); },
|
|
135
|
+
close(_args, _optional = {}) { end({ status: 'abandoned' }); },
|
|
136
|
+
expiresAtMs() { return Math.min(idleDeadline, lifetimeDeadline); },
|
|
91
137
|
};
|
|
92
138
|
},
|
|
93
139
|
deliver(requiredArgs, optionalArgs = {}) {
|
|
94
140
|
const spec = { ...requiredArgs, ...optionalArgs };
|
|
95
141
|
const entry = openExchanges.get(spec.exchangeId);
|
|
142
|
+
// Unknown and closed are the same situation to a human whose dialog is no longer waiting.
|
|
96
143
|
if (!entry)
|
|
97
144
|
return { ok: false, reason: 'unknown-or-closed' };
|
|
98
145
|
if ((spec.toolId !== undefined && entry.binding.toolId !== spec.toolId)
|
|
99
146
|
|| (entry.binding.channel ?? deps.defaultChannel) !== (spec.channel ?? deps.defaultChannel)
|
|
100
|
-
|| entry.binding.principalId !== spec.principalId)
|
|
147
|
+
|| entry.binding.principalId !== spec.principalId) {
|
|
148
|
+
// A wrong-binding delivery must not consume a conversation the right human is still having.
|
|
101
149
|
return { ok: false, reason: 'binding-mismatch' };
|
|
150
|
+
}
|
|
102
151
|
entry.accept(spec.params);
|
|
103
152
|
return openExchanges.has(spec.exchangeId) ? { ok: true } : { ok: false, reason: 'unknown-or-closed' };
|
|
104
153
|
},
|
|
105
|
-
findTypedAnswerTarget({ principalId, toolId }) {
|
|
154
|
+
findTypedAnswerTarget({ principalId, toolId }, _optional = {}) {
|
|
106
155
|
let found;
|
|
107
156
|
for (const [id, entry] of openExchanges) {
|
|
108
157
|
if (entry.binding.principalId !== principalId || entry.binding.toolId !== toolId)
|
|
109
158
|
continue;
|
|
159
|
+
// Two questions for the same human and tool are ambiguous; never guess which prose answers.
|
|
110
160
|
if (found !== undefined)
|
|
111
161
|
return undefined;
|
|
112
162
|
found = id;
|
|
@@ -116,19 +166,93 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
116
166
|
size: () => openExchanges.size,
|
|
117
167
|
};
|
|
118
168
|
}
|
|
119
|
-
|
|
169
|
+
/**
|
|
170
|
+
* The one-shot case: send once, wait once, close.
|
|
171
|
+
*
|
|
172
|
+
* Kept as a helper rather than a second mechanism, because "ask a question and get one answer" is
|
|
173
|
+
* genuinely just the shortest exchange. Anything that later needs a follow-up turn stops calling this
|
|
174
|
+
* and drives the exchange directly, with no change to the store, the route, or the transport.
|
|
175
|
+
*/
|
|
176
|
+
export async function askOnce({ exchange, emission }, _optional = {}) {
|
|
120
177
|
try {
|
|
121
|
-
await
|
|
122
|
-
return await exchange.receive({});
|
|
178
|
+
return await sendThenReceive({ exchange, emission });
|
|
123
179
|
}
|
|
124
180
|
finally {
|
|
125
181
|
exchange.close({});
|
|
126
182
|
}
|
|
127
183
|
}
|
|
128
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Sends a question and waits for its answer without closing the exchange.
|
|
186
|
+
* Expiry can precede the first send while a caller builds its dialog. Return that terminal result,
|
|
187
|
+
* never emit or consume a buffered confirmation on an ended exchange; emitter failures still throw.
|
|
188
|
+
* @complexity O(1) plus the emitter's cost; no additional buffering or timers.
|
|
189
|
+
*/
|
|
190
|
+
async function sendThenReceive({ exchange, emission }, _optional = {}) {
|
|
191
|
+
try {
|
|
192
|
+
await exchange.send({ emission });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof SurfaceExchangeEndedError)
|
|
196
|
+
return error.terminal;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
return exchange.receive({});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Like {@link askOnce}, but for a call whose answer triggers real work whose OUTCOME the human also
|
|
203
|
+
* needs to see — a publish, a delete, a credential save. `askOnce` alone cannot express this: it
|
|
204
|
+
* closes the exchange the instant an answer arrives, so a caller that awaited it and then tried to
|
|
205
|
+
* `exchange.send()` a result would find the exchange already dead (`send()` throws "already ended").
|
|
206
|
+
*
|
|
207
|
+
* ## The defect this exists to fix
|
|
208
|
+
*
|
|
209
|
+
* A confirmation surface's own script (`confirmation.ts`) sets its status text to "Done." the moment
|
|
210
|
+
* the confirming `tools/call` RESOLVES — and for a held-open exchange delivered through
|
|
211
|
+
* `mcp-ui-tool-calls-route.ts`'s Shape 1, that call resolves the instant the click is DELIVERED to
|
|
212
|
+
* the parked agent call (`{delivered: true}`, HTTP 202), which happens before the parked handler has
|
|
213
|
+
* done anything with the answer yet, let alone finished. "Done." therefore means "your click
|
|
214
|
+
* reached the server," never "the thing you asked for actually happened" — a 404, a rejected
|
|
215
|
+
* credential, and a genuine success all render the identical "Done." to the human. `handle` below is
|
|
216
|
+
* where a caller does the real work and decides what actually happened; the outcome emission this
|
|
217
|
+
* function sends afterward is what corrects the record.
|
|
218
|
+
*
|
|
219
|
+
* ## How the correction reaches the screen
|
|
220
|
+
*
|
|
221
|
+
* The outcome emission is expected to reuse the SAME `ui://` URI the confirmation was sent under.
|
|
222
|
+
* `McpUiSurfaceCard` (`@jini-ai/chat`) already collapses a stream to the newest resource per URI —
|
|
223
|
+
* "a stream may re-send an updated document for a surface already on screen (a confirmation that
|
|
224
|
+
* became a result), so the LAST event for each URI wins" (that component's own doc) — and
|
|
225
|
+
* `McpUiHost` keys its iframe by `` `${uri}:${documentText}` ``, so a new document forces a clean
|
|
226
|
+
* remount rather than leaving the confirmation's stale Confirm/Cancel buttons on screen. Neither of
|
|
227
|
+
* those needed a change for this to work; this function only needed to make the SECOND send possible.
|
|
228
|
+
*
|
|
229
|
+
* ## Why `handle` returns the outcome rather than the caller sending it separately
|
|
230
|
+
*
|
|
231
|
+
* Keeping the send here, not in the caller, is what makes the try/catch below unconditional: a
|
|
232
|
+
* caller could forget to wrap its own `exchange.send()`, but every caller of this function gets the
|
|
233
|
+
* protection for free. `outcome` is OPTIONAL in the return — a cancelled/expired/abandoned answer has
|
|
234
|
+
* nothing to correct (the confirmation's own script already reports "Dismissed." for a local cancel,
|
|
235
|
+
* and an exchange that ended via a timeout is already closed, so `send()` would throw anyway); a
|
|
236
|
+
* caller for that branch simply omits it.
|
|
237
|
+
*
|
|
238
|
+
* ## The one invariant this function protects
|
|
239
|
+
*
|
|
240
|
+
* `result` — what reaches the model — must NEVER depend on the outcome emission actually reaching a
|
|
241
|
+
* screen. A human who closed the tab, a `send()` racing a teardown, or any other failure in the
|
|
242
|
+
* human-visible half must be invisible to the tool's own JSON contract, which is why the outcome send
|
|
243
|
+
* is wrapped in its own `try`/`catch` rather than sharing `handle`'s.
|
|
244
|
+
*
|
|
245
|
+
* @param exchange - Opened the same way `askOnce` expects one.
|
|
246
|
+
* @param confirmationEmission - Sent once, exactly like `askOnce`'s own `emission` parameter.
|
|
247
|
+
* @param handle - Runs after the answer arrives (whatever it is — received, expired, abandoned).
|
|
248
|
+
* Does the caller's real work and returns `result` (returned to THIS function's own caller,
|
|
249
|
+
* ultimately the model) plus an optional `outcome` emission to report back to the human.
|
|
250
|
+
* @complexity O(1) plus whatever `handle` itself costs — this function adds one conditional send and
|
|
251
|
+
* the same `close()` `askOnce` already pays.
|
|
252
|
+
*/
|
|
253
|
+
export async function askThenReport({ exchange, confirmationEmission, handle }, _optional = {}) {
|
|
129
254
|
try {
|
|
130
|
-
await
|
|
131
|
-
const { result, outcome } = await handle(await exchange.receive({}));
|
|
255
|
+
const { result, outcome } = await handle(await sendThenReceive({ exchange, emission: confirmationEmission }));
|
|
132
256
|
if (outcome !== undefined) {
|
|
133
257
|
try {
|
|
134
258
|
await exchange.send({ emission: outcome });
|
|
@@ -141,13 +265,36 @@ export async function askThenReport({ exchange, confirmationEmission, handle })
|
|
|
141
265
|
exchange.close({});
|
|
142
266
|
}
|
|
143
267
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Classifies an already-received {@link SurfaceMessage} as confirmed or not.
|
|
270
|
+
*
|
|
271
|
+
* Pure and synchronous on purpose: a caller that already holds an `answer` from somewhere other than
|
|
272
|
+
* a fresh {@link askOnce} call — `askThenReport`'s own `handle` callback, for instance, which receives
|
|
273
|
+
* the message directly because a second `exchange.send()` still needs to happen afterward — can
|
|
274
|
+
* classify it without a second round trip. {@link resolveConfirmationDecision} below is the `askOnce`
|
|
275
|
+
* convenience built on top of this for callers that do not need that.
|
|
276
|
+
*
|
|
277
|
+
* Fail-closed is the entire contract: only a `params.decision` field that is literally the string
|
|
278
|
+
* `"confirm"` proceeds. A missing field, a non-string value, or any other string must never be read
|
|
279
|
+
* as consent for a destructive or externally-visible action.
|
|
280
|
+
*
|
|
281
|
+
* @complexity O(1).
|
|
282
|
+
*/
|
|
283
|
+
export function classifyConfirmationAnswer({ answer }, _optional = {}) {
|
|
146
284
|
if (answer.status !== 'received')
|
|
147
285
|
return { confirmed: false, reason: answer.status };
|
|
148
286
|
return answer.params['decision'] === 'confirm' ? { confirmed: true } : { confirmed: false, reason: 'declined' };
|
|
149
287
|
}
|
|
150
|
-
|
|
288
|
+
/**
|
|
289
|
+
* {@link askOnce} plus {@link classifyConfirmationAnswer} — the whole "ask, then fail-closed classify"
|
|
290
|
+
* sequence every confirm/cancel-shaped delete/commit/publish tool repeats. A caller still owns its own
|
|
291
|
+
* domain-specific result shape for each branch (see e.g. `features/post/tool-registrations.ts`'s own
|
|
292
|
+
* `resolveDeleteDecision`) — this only shares the parked-ask-and-classify mechanics, not the result,
|
|
293
|
+
* so each site's `deleted`/`committed`/`published`/... wording stays exactly what it was.
|
|
294
|
+
*
|
|
295
|
+
* @complexity O(1) plus whatever {@link askOnce} costs.
|
|
296
|
+
*/
|
|
297
|
+
export async function resolveConfirmationDecision(args, _optional = {}) {
|
|
151
298
|
return classifyConfirmationAnswer({ answer: await askOnce(args) });
|
|
152
299
|
}
|
|
153
300
|
//# sourceMappingURL=surface-exchanges.js.map
|
package/dist/tool-audit.d.ts
CHANGED
|
@@ -1,114 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ConfirmationDecision, ToolExecutionAuditRecord, ToolExecutionPhase, ToolExecutionResult, ToolExecutionStatus, ToolExecutor } from './tool-executor.js';
|
|
3
|
-
/** The daemon vocabulary, plus attempts that never acquired an execution ID. */
|
|
4
|
-
export type ToolAttemptPhase = Extract<ToolExecutionPhase, 'requested' | ToolExecutionStatus> | 'unknown-tool';
|
|
5
|
-
export declare const TOOL_ATTEMPT_PHASES: readonly ["requested", "completed", "denied", "confirmation-denied", "timed-out", "cancelled", "failed", "unknown-tool"];
|
|
6
|
-
export interface ToolAttemptEvent {
|
|
7
|
-
readonly attemptId: string;
|
|
8
|
-
readonly executionId: string | null;
|
|
9
|
-
readonly workspaceId: string;
|
|
10
|
-
readonly runId: string;
|
|
11
|
-
readonly toolId: string;
|
|
12
|
-
readonly principalId: string;
|
|
13
|
-
readonly phase: ToolAttemptPhase;
|
|
14
|
-
readonly at: string;
|
|
15
|
-
/** Value-free metadata; never raw input, query, output or error text. */
|
|
16
|
-
readonly detail?: string | null;
|
|
17
|
-
}
|
|
18
|
-
export interface ToolAttemptAuditSink {
|
|
19
|
-
append(requiredArgs: Pick<ToolAttemptEvent, "attemptId" | "executionId" | "workspaceId" | "runId" | "toolId" | "principalId" | "phase" | "at">, optionalArgs?: Pick<ToolAttemptEvent, "detail">): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
export interface ToolAuditDependencies {
|
|
22
|
-
readonly sink: ToolAttemptAuditSink;
|
|
23
|
-
readonly now: () => string;
|
|
24
|
-
readonly newAttemptId: () => string;
|
|
25
|
-
}
|
|
26
|
-
export interface ToolAuditOptions {
|
|
27
|
-
readonly onSinkError?: (error: unknown) => void;
|
|
28
|
-
}
|
|
29
|
-
export declare function describeInput({ input }: {
|
|
30
|
-
readonly input: unknown;
|
|
31
|
-
}): string;
|
|
32
|
-
export interface AuditedToolExecutor {
|
|
33
|
-
execute(args: {
|
|
34
|
-
principal: Principal;
|
|
35
|
-
run: RunRef;
|
|
36
|
-
toolId: string;
|
|
37
|
-
input: unknown;
|
|
38
|
-
}, options?: {
|
|
39
|
-
signal?: AbortSignal;
|
|
40
|
-
emitSurface?: SurfaceEmitter;
|
|
41
|
-
}): Promise<ToolExecutionResult>;
|
|
42
|
-
resumeConfirmation(args: {
|
|
43
|
-
executionId: string;
|
|
44
|
-
decision: ConfirmationDecision;
|
|
45
|
-
}): void;
|
|
46
|
-
cancel(args: {
|
|
47
|
-
executionId: string;
|
|
48
|
-
}): void;
|
|
49
|
-
getAuditRecord(args: {
|
|
50
|
-
executionId: string;
|
|
51
|
-
}): ToolExecutionAuditRecord | null;
|
|
52
|
-
}
|
|
53
|
-
/** Wraps the existing boundary without creating another executor or another phase vocabulary. */
|
|
54
|
-
export declare function withToolAttemptAudit(deps: ToolAuditDependencies & {
|
|
55
|
-
readonly inner: ToolExecutor;
|
|
56
|
-
readonly workspaceId: string;
|
|
57
|
-
readonly readErrorId: (args: {
|
|
58
|
-
result: ToolExecutionResult;
|
|
59
|
-
}) => string | undefined;
|
|
60
|
-
}, options?: ToolAuditOptions): AuditedToolExecutor;
|
|
61
|
-
export declare function searchToolsAuditDetail({ query, limit, hits }: {
|
|
62
|
-
query: string;
|
|
63
|
-
limit: number | null;
|
|
64
|
-
hits: readonly {
|
|
65
|
-
id: string;
|
|
66
|
-
}[];
|
|
67
|
-
}): string;
|
|
68
|
-
export declare function describeToolAuditDetail({ id, entry }: {
|
|
69
|
-
id: string;
|
|
70
|
-
entry: unknown | null;
|
|
71
|
-
}): string;
|
|
72
|
-
export interface ToolCatalogAuditIdentity {
|
|
73
|
-
readonly workspaceId: string;
|
|
74
|
-
readonly runId: string;
|
|
75
|
-
readonly principalId: string;
|
|
76
|
-
}
|
|
77
|
-
export interface ToolCatalogAttemptEvent extends ToolCatalogAuditIdentity {
|
|
78
|
-
readonly toolId: string;
|
|
79
|
-
readonly detail: string;
|
|
80
|
-
}
|
|
81
|
-
export declare function appendToolCatalogAttempt(deps: ToolAuditDependencies & {
|
|
82
|
-
readonly event: ToolCatalogAttemptEvent;
|
|
83
|
-
}, options?: ToolAuditOptions): void;
|
|
84
|
-
/** Structural source port: no database or catalog-provider dependency. */
|
|
85
|
-
export interface ToolCatalogAuditSource<Hit extends {
|
|
86
|
-
id: string;
|
|
87
|
-
}, Entry> {
|
|
88
|
-
search(args: {
|
|
89
|
-
query: string;
|
|
90
|
-
}, options?: {
|
|
91
|
-
limit?: number;
|
|
92
|
-
}): readonly Hit[];
|
|
93
|
-
describe(args: {
|
|
94
|
-
id: string;
|
|
95
|
-
}): Entry | null;
|
|
96
|
-
}
|
|
97
|
-
export declare function withToolCatalogAudit<Hit extends {
|
|
98
|
-
id: string;
|
|
99
|
-
}, Entry>(deps: ToolAuditDependencies & {
|
|
100
|
-
catalog: ToolCatalogAuditSource<Hit, Entry>;
|
|
101
|
-
identity: ToolCatalogAuditIdentity;
|
|
102
|
-
searchToolId: string;
|
|
103
|
-
describeToolId: string;
|
|
104
|
-
}, options?: ToolAuditOptions): {
|
|
105
|
-
search({ query }: {
|
|
106
|
-
query: string;
|
|
107
|
-
}, { limit }?: {
|
|
108
|
-
limit?: number;
|
|
109
|
-
}): readonly Hit[];
|
|
110
|
-
describe({ id }: {
|
|
111
|
-
id: string;
|
|
112
|
-
}): Entry | null;
|
|
113
|
-
};
|
|
1
|
+
export * from './core/tool-audit.js';
|
|
114
2
|
//# sourceMappingURL=tool-audit.d.ts.map
|
package/dist/tool-audit.js
CHANGED
|
@@ -1,95 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
];
|
|
4
|
-
function reportSinkError(error, options) {
|
|
5
|
-
// An observer's reporter is itself an observer: a throwing reporter cannot gate a tool.
|
|
6
|
-
try {
|
|
7
|
-
options.onSinkError?.(error);
|
|
8
|
-
}
|
|
9
|
-
catch { /* execution remains authoritative */ }
|
|
10
|
-
}
|
|
11
|
-
async function appendSafely(event, deps, options) {
|
|
12
|
-
try {
|
|
13
|
-
await deps.sink.append(event);
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
reportSinkError(error, options);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function describeInput({ input }) {
|
|
20
|
-
if (input === null || input === undefined)
|
|
21
|
-
return `input: ${String(input)}`;
|
|
22
|
-
if (Array.isArray(input))
|
|
23
|
-
return `input: an array of ${input.length}`;
|
|
24
|
-
if (typeof input !== 'object')
|
|
25
|
-
return `input: a ${typeof input}`;
|
|
26
|
-
try {
|
|
27
|
-
const parts = Object.entries(input).map(([key, value]) => Array.isArray(value) ? `${key}[${value.length}]` : key).sort();
|
|
28
|
-
return parts.length === 0 ? 'keys: none' : `keys: ${parts.join(', ')}`;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return 'input: an unreadable object';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/** Wraps the existing boundary without creating another executor or another phase vocabulary. */
|
|
35
|
-
export function withToolAttemptAudit(deps, options = {}) {
|
|
36
|
-
return {
|
|
37
|
-
async execute({ principal, run, toolId, input }, { signal, emitSurface } = {}) {
|
|
38
|
-
const base = { attemptId: deps.newAttemptId(), workspaceId: deps.workspaceId, runId: run.id, toolId, principalId: principal.id };
|
|
39
|
-
await appendSafely({ ...base, executionId: null, phase: 'requested', at: deps.now(), detail: describeInput({ input }) }, deps, options);
|
|
40
|
-
let result;
|
|
41
|
-
try {
|
|
42
|
-
result = await deps.inner.execute({ principal, run, toolId, input }, {
|
|
43
|
-
...(signal === undefined ? {} : { signal }),
|
|
44
|
-
...(emitSurface === undefined ? {} : { emitSurface }),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
const phase = error instanceof Error && /unknown tool/i.test(error.message) ? 'unknown-tool' : 'failed';
|
|
49
|
-
await appendSafely({ ...base, executionId: null, phase, at: deps.now(), detail: error instanceof Error ? error.name : typeof error }, deps, options);
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
let detail = result.truncated ? 'output truncated' : null;
|
|
53
|
-
// Host error-ID metadata is optional observation, never part of execution success.
|
|
54
|
-
if (detail === null) {
|
|
55
|
-
try {
|
|
56
|
-
const id = deps.readErrorId({ result });
|
|
57
|
-
if (id !== undefined)
|
|
58
|
-
detail = `errorId=${id}`;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
reportSinkError(error, options);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
await appendSafely({ ...base, executionId: result.executionId, phase: result.status, at: deps.now(), detail }, deps, options);
|
|
65
|
-
return result;
|
|
66
|
-
},
|
|
67
|
-
resumeConfirmation: ({ executionId, decision }) => deps.inner.resumeConfirmation({ executionId: executionId, decision: decision }),
|
|
68
|
-
cancel: ({ executionId }) => deps.inner.cancel({ executionId }),
|
|
69
|
-
getAuditRecord: ({ executionId }) => deps.inner.getAuditRecord({ executionId: executionId }),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
export function searchToolsAuditDetail({ query, limit, hits }) {
|
|
73
|
-
return JSON.stringify({ queryLength: query.length, limit, resultIds: hits.map(hit => hit.id), resultCount: hits.length });
|
|
74
|
-
}
|
|
75
|
-
export function describeToolAuditDetail({ id, entry }) {
|
|
76
|
-
return JSON.stringify({ id, found: entry !== null });
|
|
77
|
-
}
|
|
78
|
-
export function appendToolCatalogAttempt(deps, options = {}) {
|
|
79
|
-
void appendSafely({ ...deps.event, attemptId: deps.newAttemptId(), executionId: null, phase: 'completed', at: deps.now() }, deps, options);
|
|
80
|
-
}
|
|
81
|
-
export function withToolCatalogAudit(deps, options = {}) {
|
|
82
|
-
return {
|
|
83
|
-
search({ query }, { limit } = {}) {
|
|
84
|
-
const hits = deps.catalog.search({ query }, limit === undefined ? {} : { limit });
|
|
85
|
-
appendToolCatalogAttempt({ ...deps, event: { ...deps.identity, toolId: deps.searchToolId, detail: searchToolsAuditDetail({ query, limit: limit ?? null, hits }) } }, options);
|
|
86
|
-
return hits;
|
|
87
|
-
},
|
|
88
|
-
describe({ id }) {
|
|
89
|
-
const entry = deps.catalog.describe({ id });
|
|
90
|
-
appendToolCatalogAttempt({ ...deps, event: { ...deps.identity, toolId: deps.describeToolId, detail: describeToolAuditDetail({ id, entry }) } }, options);
|
|
91
|
-
return entry;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
1
|
+
// Shared audit owner; used by standalone auditing and the recovery stack.
|
|
2
|
+
export * from './core/tool-audit.js';
|
|
95
3
|
//# sourceMappingURL=tool-audit.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The general tool-failure contract (2026-09-01): what ANY tool attaches to a failed outcome
|
|
3
|
+
* so a caller — human, agent, or an automated retry loop — has something to reason about instead of a
|
|
4
|
+
* bare status code with nothing behind it.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* Hand-writing a bespoke recovery path per failure mode does not scale — there will always be more
|
|
9
|
+
* flaws, gaps, and provider quirks than anyone can anticipate and special-case individually. The
|
|
10
|
+
* leverage is not a smarter model reading a smarter error message; it is a SHARED SHAPE every tool's
|
|
11
|
+
* failure can be poured into, so one generic loop — read the failure, ask the human through a
|
|
12
|
+
* surface, apply the answer, retry once — can handle a failure mode nobody wrote code for, using the
|
|
13
|
+
* identical mechanism that already handles one somebody did.
|
|
14
|
+
*
|
|
15
|
+
* ## The four things a diagnostic carries
|
|
16
|
+
*
|
|
17
|
+
* 1. **What failed.** Deliberately NOT modeled in this shared contract — it is always domain-specific,
|
|
18
|
+
* and forcing every failure mode into one generic "what failed" string or object would either
|
|
19
|
+
* flatten real structure that needs to stay queryable (a concrete diagnostic's own fact fields —
|
|
20
|
+
* see {@link AuthFailureDiagnostic}'s `schemeSent`/`usernameStored` in `features/custom-credentials/
|
|
21
|
+
* credentialed-request.ts` for the worked example) or force a restructuring this file has no
|
|
22
|
+
* business demanding of every domain that wants to adopt it. A concrete diagnostic type carries
|
|
23
|
+
* its own facts as its own additional fields, declared alongside the two this contract does own.
|
|
24
|
+
* 2. **The best hypothesis why** — folded into {@link ToolFailureDiagnostic.hint}.
|
|
25
|
+
* 3. **What input or state would change the outcome** — also folded into `hint`, not a separate field;
|
|
26
|
+
* see that property's own doc for why the two live together.
|
|
27
|
+
* 4. **Whether a registered tool can supply it** — {@link ToolFailureDiagnostic.remedyToolId}.
|
|
28
|
+
*
|
|
29
|
+
* ## Built from one worked example, not designed from zero
|
|
30
|
+
*
|
|
31
|
+
* `AuthFailureDiagnostic` is this contract's first instance, extracted AFTER it shipped and proved
|
|
32
|
+
* itself against a real incident — not designed up front from a guess at what failures might someday
|
|
33
|
+
* look like. See that type's own file header, "Authentication-failure diagnostics", for the incident
|
|
34
|
+
* and for the exact honesty contract every one of its fields is held to. This file generalizes its
|
|
35
|
+
* SHAPE, not its content: the fields that type adds (`schemeSent`, `usernameStored`) and the exact
|
|
36
|
+
* wording of its `hint` are unchanged by this extraction.
|
|
37
|
+
*
|
|
38
|
+
* ## The honesty contract every field is held to
|
|
39
|
+
*
|
|
40
|
+
* - **Silence is the default, not an afterthought.** Both fields below are optional, and absent
|
|
41
|
+
* together far more often than present: a diagnostic type should fill them in for the ONE narrow
|
|
42
|
+
* shape it can honestly speak to, and stay silent for every other shape — including shapes that
|
|
43
|
+
* look superficially similar. Repeating advice that has already been tried and failed, or guessing
|
|
44
|
+
* at a cause this diagnostic cannot actually tell apart from some other one, is a false lead, not
|
|
45
|
+
* merely an unhelpful one.
|
|
46
|
+
* - **Never a claim.** `hint`, when present, is hedged ("may"/"might"/"could") — a hypothesis for a
|
|
47
|
+
* human or agent to try, never an assertion of the real cause.
|
|
48
|
+
* - **Additive, never a replacement.** A diagnostic rides ALONGSIDE the tool's own real failure output
|
|
49
|
+
* (a provider's response body, a thrown error's message) — it never suppresses or reinterprets it.
|
|
50
|
+
* - **`hint` and `remedyToolId` travel as a pair, never `remedyToolId` alone.** A tool id with nothing
|
|
51
|
+
* explaining why it might help is a suggestion with no argument behind it, not a diagnostic. The
|
|
52
|
+
* reverse is fine: `hint` with no `remedyToolId` simply means no registered tool can supply the fix
|
|
53
|
+
* yet — the hypothesis is still worth surfacing to a human who can act on it by hand.
|
|
54
|
+
*
|
|
55
|
+
* ## The one-cycle guard
|
|
56
|
+
*
|
|
57
|
+
* `remedyToolId` is a POINTER — a tool id, nothing else. It carries no callback, no arguments, and no
|
|
58
|
+
* instruction to invoke it automatically. That is deliberate: the consuming loop (ask the human → apply
|
|
59
|
+
* the answer through the named tool → retry ONCE) owns the decision of whether and how to act on it,
|
|
60
|
+
* and owns the guard against a second cycle. A diagnostic that carried more than an id — an executable
|
|
61
|
+
* action, say — would let that guard slip into this file instead of staying where the actual retry
|
|
62
|
+
* happens, turning a bug in this contract into a genuine loop instead of a single bounded retry.
|
|
63
|
+
*
|
|
64
|
+
* ## Architectural role
|
|
65
|
+
*
|
|
66
|
+
* `contracts/core`, domain-agnostic — it names no post, page, credential, or other domain concept, so
|
|
67
|
+
* a failure mode in any feature can adopt it without this file growing a dependency on that feature.
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* A tool-failure diagnostic's shared, domain-agnostic half. A concrete diagnostic type extends this
|
|
71
|
+
* with whatever structured facts its own failure mode requires (facet 1, "what failed" — see this
|
|
72
|
+
* file's header) as additional fields alongside the two declared here.
|
|
73
|
+
*/
|
|
74
|
+
export interface ToolFailureDiagnostic {
|
|
75
|
+
readonly setupToolId?: string;
|
|
76
|
+
readonly prefill?: Readonly<Record<string, string | number | boolean | null>>;
|
|
77
|
+
/**
|
|
78
|
+
* The best hypothesis for why the call failed, together with what input or state would change the
|
|
79
|
+
* outcome if that hypothesis is right (facets 2 and 3 of this file's header) — one hedged sentence,
|
|
80
|
+
* never a bare claim. Omitted entirely — not filled with a guess — whenever the failure could have
|
|
81
|
+
* more than one honest explanation from here. See this file's header, "The honesty contract every
|
|
82
|
+
* field is held to".
|
|
83
|
+
*
|
|
84
|
+
* Folded into one field rather than split into separate `hypothesis`/`remedy` fields: every diagnosis
|
|
85
|
+
* this contract has actually had to express so far is a single hedged sentence that names both in
|
|
86
|
+
* the same breath ("X may be why — saving Y may fix it"), and splitting a hedge into two structured
|
|
87
|
+
* fields would invite treating the "remedy" half as more certain than the hedge it depends on.
|
|
88
|
+
*/
|
|
89
|
+
readonly hint?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The id of an already-registered tool that could supply the fix `hint` describes, when one exists
|
|
92
|
+
* (facet 4 of this file's header) — a pointer only, never a callback; see this file's header, "The
|
|
93
|
+
* one-cycle guard". Present only alongside `hint` — never on its own.
|
|
94
|
+
*/
|
|
95
|
+
readonly remedyToolId?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Marks `diagnostic` as issued by the calling tool for a real failure, so the recovery loop
|
|
99
|
+
* (`assistant/tool-failure-recovery.ts`) may act on it. Returns the same object. Call it where the
|
|
100
|
+
* diagnostic is built, and return that object itself: a copy (spread, JSON round trip) is not issued.
|
|
101
|
+
*
|
|
102
|
+
* @complexity O(1).
|
|
103
|
+
*/
|
|
104
|
+
export declare function issueToolFailureDiagnostic<T extends ToolFailureDiagnostic>({ diagnostic }: {
|
|
105
|
+
diagnostic: T;
|
|
106
|
+
}, _optional?: {}): T;
|
|
107
|
+
/**
|
|
108
|
+
* True only for an object {@link issueToolFailureDiagnostic} marked.
|
|
109
|
+
*
|
|
110
|
+
* @complexity O(1).
|
|
111
|
+
*/
|
|
112
|
+
export declare function isIssuedToolFailureDiagnostic({ value }: {
|
|
113
|
+
value: unknown;
|
|
114
|
+
}, _optional?: {}): boolean;
|
|
115
|
+
/** Only card metadata travels through recovery; secret values belong to the parked card. */
|
|
116
|
+
export declare function issueCredentialSetup(required: {
|
|
117
|
+
setupToolId: string;
|
|
118
|
+
prefill: Readonly<Record<string, string | number | boolean | null>>;
|
|
119
|
+
assertCredentialFreeField: (required: {
|
|
120
|
+
value: string;
|
|
121
|
+
field: string;
|
|
122
|
+
}, optional: Record<string, never>) => void;
|
|
123
|
+
containsSecret: (required: {
|
|
124
|
+
text: string;
|
|
125
|
+
}) => boolean;
|
|
126
|
+
}, _optional?: {}): ToolFailureDiagnostic & {
|
|
127
|
+
setupToolId: string;
|
|
128
|
+
prefill: typeof required.prefill;
|
|
129
|
+
hint: string;
|
|
130
|
+
remedyToolId: string;
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|