@jini-ai/daemon 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/agent-executor/contracts.d.ts +26 -20
  3. package/dist/agent-executor/contracts.js +0 -9
  4. package/dist/agent-executor/event-translation.d.ts +2 -2
  5. package/dist/agent-executor/event-translation.js +1 -2
  6. package/dist/agent-executor/events.d.ts +9 -4
  7. package/dist/agent-executor/events.js +29 -7
  8. package/dist/agent-executor/handoff.d.ts +1 -1
  9. package/dist/agent-executor/handoff.js +0 -1
  10. package/dist/agent-executor/launch.d.ts +1 -1
  11. package/dist/agent-executor/launch.js +24 -1
  12. package/dist/agent-executor/mcp-bridge.d.ts +2 -2
  13. package/dist/agent-executor/mcp-bridge.js +0 -2
  14. package/dist/agent-executor/model-receipts.d.ts +6 -0
  15. package/dist/agent-executor/model-receipts.js +26 -0
  16. package/dist/agent-executor.d.ts +8 -1663
  17. package/dist/agent-executor.js +8 -3005
  18. package/dist/attachment-content.d.ts +45 -0
  19. package/dist/attachment-content.js +40 -0
  20. package/dist/core/read-only-tools.d.ts +130 -0
  21. package/dist/core/read-only-tools.js +115 -0
  22. package/dist/core/tool-audit.d.ts +131 -0
  23. package/dist/core/tool-audit.js +113 -0
  24. package/dist/http/active-context.d.ts +2 -2
  25. package/dist/http/agents.d.ts +8 -2
  26. package/dist/http/agents.js +6 -2
  27. package/dist/http/attachments.d.ts +16 -7
  28. package/dist/http/attachments.js +3 -2
  29. package/dist/http/component-catalog.d.ts +2 -2
  30. package/dist/http/connectors.d.ts +17 -17
  31. package/dist/http/daemon-status.d.ts +2 -2
  32. package/dist/http/db-ops.d.ts +3 -3
  33. package/dist/http/delegated-tools.d.ts +1 -1
  34. package/dist/http/delegated-tools.js +1 -1
  35. package/dist/http/frontend-sessions.d.ts +1 -1
  36. package/dist/http/health.d.ts +6 -6
  37. package/dist/http/host-tools.d.ts +2 -2
  38. package/dist/http/media.d.ts +4 -4
  39. package/dist/http/memory.d.ts +15 -15
  40. package/dist/http/remote-run-events.d.ts +2 -2
  41. package/dist/http/research.d.ts +1 -1
  42. package/dist/http/routines.d.ts +7 -7
  43. package/dist/http/run-credentials.d.ts +2 -2
  44. package/dist/http/run-credentials.js +2 -2
  45. package/dist/http/runs.d.ts +4 -4
  46. package/dist/http/terminals.d.ts +6 -6
  47. package/dist/http/tool-catalog.d.ts +2 -2
  48. package/dist/http/xai.d.ts +6 -6
  49. package/dist/index.d.ts +76 -571
  50. package/dist/index.js +76 -571
  51. package/dist/read-only-tools.d.ts +1 -129
  52. package/dist/read-only-tools.js +2 -114
  53. package/dist/run-credentials.d.ts +47 -23
  54. package/dist/run-credentials.js +24 -8
  55. package/dist/run-lifecycle.d.ts +1 -1
  56. package/dist/run-lifecycle.js +6 -5
  57. package/dist/scheduler.d.ts +5 -0
  58. package/dist/scheduler.js +13 -1
  59. package/dist/session-coordination.d.ts +58 -24
  60. package/dist/session-coordination.js +40 -12
  61. package/dist/store/event-log/sqlite.js +3 -3
  62. package/dist/surface-exchanges.d.ts +207 -61
  63. package/dist/surface-exchanges.js +167 -20
  64. package/dist/tool-audit.d.ts +1 -113
  65. package/dist/tool-audit.js +2 -94
  66. package/dist/tool-recovery/diagnostics.d.ts +132 -0
  67. package/dist/tool-recovery/diagnostics.js +110 -0
  68. package/dist/tool-recovery/federated-refusal.d.ts +92 -0
  69. package/dist/tool-recovery/federated-refusal.js +152 -0
  70. package/dist/tool-recovery/index.d.ts +6 -0
  71. package/dist/tool-recovery/index.js +6 -0
  72. package/dist/tool-recovery/recovery.d.ts +49 -0
  73. package/dist/tool-recovery/recovery.js +419 -0
  74. package/dist/tool-recovery/redaction.d.ts +166 -0
  75. package/dist/tool-recovery/redaction.js +192 -0
  76. package/dist/tool-recovery/stack.d.ts +69 -0
  77. package/dist/tool-recovery/stack.js +62 -0
  78. package/package.json +35 -12
@@ -1,15 +1,58 @@
1
- export const SURFACE_EXCHANGE_ID_PARAM = '__exchangeId';
2
- export const SURFACE_DISMISSED_PARAM = '__dismissed';
3
- export const SURFACE_TYPED_ANSWER_PARAM = '__typedAnswer';
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
- /** In-process held calls. Inject a scheduler that unrefs its timers where appropriate. */
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 Error(`surface exchange ${id} has already ended`);
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
- export async function askOnce({ exchange, emission }) {
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 exchange.send({ emission });
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
- export async function askThenReport({ exchange, confirmationEmission, handle }) {
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 exchange.send({ emission: confirmationEmission });
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
- /** Typed prose is never consent; only an explicit literal decision grants approval. */
145
- export function classifyConfirmationAnswer({ answer }) {
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
- export async function resolveConfirmationDecision(args) {
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
@@ -1,114 +1,2 @@
1
- import type { Principal, RunRef, SurfaceEmitter } from '@jini-ai/core';
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
@@ -1,95 +1,3 @@
1
- export const TOOL_ATTEMPT_PHASES = [
2
- 'requested', 'completed', 'denied', 'confirmation-denied', 'timed-out', 'cancelled', 'failed', 'unknown-tool',
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