@decidio/sdk 0.1.12 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/errors.js CHANGED
@@ -1,18 +1,50 @@
1
+ const RETRYABLE_STATUS = (status) => status === 408 || status === 429 || (status >= 500 && status !== 501);
2
+ function sideEffectNote(endpoint) {
3
+ if (endpoint.startsWith("/agent/confirm")) {
4
+ return " Your action ALREADY RAN — only its report failed. Do NOT re-run it.";
5
+ }
6
+ if (endpoint.startsWith("/agent/status")) {
7
+ return " This call is a read, so it changed nothing; the decision itself is unaffected.";
8
+ }
9
+ return " Nothing was routed or executed.";
10
+ }
1
11
  export class DecidioHttpError extends Error {
2
12
  status;
3
13
  endpoint;
4
14
  body;
5
15
  retryable;
6
16
  constructor(status, endpoint, body) {
7
- const terminal = status === 401 || status === 403;
17
+ const credential = status === 401 || status === 403;
18
+ const retryable = !credential && RETRYABLE_STATUS(status);
8
19
  super(`[decidio] ${endpoint} refused this call (HTTP ${status})` +
9
- (terminal
10
- ? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process. Nothing was routed or executed.`
11
- : ` — this looks transient; retrying is safe (a retry creates a NEW request and can never double-execute). Nothing was routed or executed.`));
20
+ (credential
21
+ ? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process.`
22
+ : retryable
23
+ ? ` — this looks transient; retrying is safe (a retry creates a NEW request and can never double-execute).`
24
+ : ` — the server refused the request itself, so retrying it unchanged will fail the same way. Fix the request (or the decision it names) first.`) +
25
+ sideEffectNote(endpoint));
12
26
  this.status = status;
13
27
  this.endpoint = endpoint;
14
28
  this.body = body;
15
29
  this.name = "DecidioHttpError";
16
- this.retryable = !terminal;
30
+ this.retryable = retryable;
31
+ }
32
+ }
33
+ export function decisionIdProblem(raw) {
34
+ if (typeof raw !== "string")
35
+ return `it is ${raw === null ? "null" : typeof raw}, not a string`;
36
+ if (raw.length === 0)
37
+ return "it is empty";
38
+ if (raw.trim().length === 0)
39
+ return "it is only whitespace";
40
+ if (raw !== raw.trim())
41
+ return "it has leading or trailing whitespace (ids are used verbatim in URLs, filenames and audit lines — this SDK will not silently trim one)";
42
+ for (const ch of raw) {
43
+ const cp = ch.codePointAt(0);
44
+ if (cp < 0x20 || cp === 0x7f)
45
+ return "it contains control characters";
17
46
  }
47
+ if (raw.length > 256)
48
+ return `it is ${raw.length} characters long (limit 256)`;
49
+ return null;
18
50
  }
@@ -17,6 +17,25 @@ export interface EvidenceOutcome {
17
17
  * proof VERIFIED (C3, 0.1.12 — this field used to be read off the wire and discarded, so a
18
18
  * failed attestation was invisible to every client door). */
19
19
  executionReportVerified?: boolean;
20
+ /** WHY the attestation did not verify, verbatim from the gate (unknown did, expired proof,
21
+ * burnt nonce, payload mismatch). It was folded into `diagnostic` prose and nowhere else, so a
22
+ * caller who wanted to BRANCH on the reason — retry vs re-register vs alert — had to regex an
23
+ * English sentence. Same rule as the typed HTTP errors: give callers the fact, not a string to
24
+ * parse. */
25
+ executionReportReason?: string;
26
+ /** Whether re-sending this report could succeed. False for a refusal the server will repeat
27
+ * (an unrecognised tier, a denied success flag); true for a transport or availability failure.
28
+ * Undefined when the report WAS recorded and there is nothing to retry. */
29
+ confirmationRetryable?: boolean;
30
+ /** Whether the reported value reached durable storage, so `reconfirm()` can re-send it. On the
31
+ * inline path this is false by construction — nothing is parked — which is exactly why an
32
+ * inline caller needs to see the outcome at the call site rather than in a log line. */
33
+ reportJournalled?: boolean;
34
+ /** Set when the caller's `evidence` mapper (or the canonical model) rejected the return value,
35
+ * so an EMPTY captured response was reported. The execution is recorded; its evidence is not.
36
+ * This was previously a console.warn and nothing else — invisible to the code that could
37
+ * actually fix it. */
38
+ evidenceError?: string;
20
39
  }
21
40
  /** The SERVER's rule for `application_confirmed`, restated here exactly as implemented so this
22
41
  * explanation can never drift from it: the captured response must carry a RECORD ID
package/dist/evidence.js CHANGED
@@ -20,19 +20,20 @@ export function describeTierGap(capturedResponse) {
20
20
  return "the report was accepted but the gate recorded a weaker tier — check that this agent's registration and signing key still match (`npx @decidio/sdk doctor`).";
21
21
  }
22
22
  export function readConfirmResponse(body, opts) {
23
- const retry = opts.journalled
23
+ const journalled = opts.journalled ?? false;
24
+ const retry = journalled
24
25
  ? "re-send the report with `guard.resume.reconfirm(decisionId)` once connectivity or the service is healthy."
25
26
  : "this path journalled no report, so there is nothing to re-send from here — record the outcome from your own system, or run the action through `guard.protect`'s durable mode, where reports ARE journalled and `reconfirm` can replay them.";
26
27
  const b = body;
27
28
  if (!b || b.ok !== true || typeof b.confirmation !== "string") {
28
- return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
29
+ return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: true, reportJournalled: journalled, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
29
30
  }
30
31
  const tier = b.confirmation;
31
32
  if (!EVIDENCE_TIERS.includes(tier)) {
32
- return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
33
+ return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: false, reportJournalled: journalled, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
33
34
  }
34
35
  const executionConfirmed = CONFIRMING.includes(tier);
35
- const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed };
36
+ const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed, reportJournalled: journalled };
36
37
  if (!executionConfirmed && opts.signed)
37
38
  outcome.diagnostic = describeTierGap(opts.capturedResponse);
38
39
  else if (!executionConfirmed)
@@ -40,6 +41,8 @@ export function readConfirmResponse(body, opts) {
40
41
  const rpt = body?.executionReport;
41
42
  if (rpt && typeof rpt === "object" && typeof rpt.verified === "boolean") {
42
43
  outcome.executionReportVerified = rpt.verified;
44
+ if (typeof rpt.reason === "string" && rpt.reason)
45
+ outcome.executionReportReason = rpt.reason;
43
46
  if (!rpt.verified) {
44
47
  const why = typeof rpt.reason === "string" && rpt.reason ? ` (${rpt.reason})` : "";
45
48
  outcome.diagnostic = `${outcome.diagnostic ? outcome.diagnostic + " " : ""}The signed execution report did NOT verify${why} — the report was recorded without a proven attestation. Check the agent's key/DID/registration (npx @decidio/sdk doctor).`;
package/dist/guard.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ApprovalContext } from "./index.js";
1
+ import { type ApprovalContext, type ProtectedExecution } from "./index.js";
2
2
  import { type ResumeController, type ResumeOutcome } from "./resume.js";
3
3
  export interface GuardFacadeConfig {
4
4
  apiUrl: string;
@@ -28,6 +28,12 @@ export interface ProtectOptions {
28
28
  * across every door), but their ENGINE integration is verified against structural mocks, not
29
29
  * live engines. Use the core path for production until that changes. */
30
30
  adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest";
31
+ /** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
32
+ * idempotencyKey }`. Pass `idempotencyKey` to your downstream API and the one window this SDK
33
+ * cannot close — the vendor committing twice — closes on their side. Opt-in by design: arity
34
+ * inspection would have to guess, and guessing wrong hands a payment call a context object
35
+ * where it expected an invoice. */
36
+ withContext?: boolean;
31
37
  /** "durable" (default) suspends for async approval; "blocking" polls (short approvals only). */
32
38
  mode?: "durable" | "blocking";
33
39
  /** The action name this wrapper produces, declared UP FRONT so the resume registry is populated
@@ -84,7 +90,18 @@ export declare class Guard {
84
90
  /** Throws when the facade has no gate URL — at USE time, not import time, so `import { guard }`
85
91
  * stays safe in envless contexts. */
86
92
  private assertConfigured;
87
- protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
93
+ /** Wrap a function so every call is authorized, recorded and — in 0.2.0 — REPORTED BACK.
94
+ *
95
+ * Returns `{ value, decisionId, confirmation }`, not the bare value. Through 0.1.x it returned
96
+ * `R` and the confirmation outcome went to a console warning, so the one thing a caller of an
97
+ * authority SDK could not do was find out what Decidio recorded. If you genuinely do not need
98
+ * it, `protectBestEffort` keeps the old shape. */
99
+ protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<ProtectedExecution<R>>;
100
+ /** @deprecated The 0.1.x shape: resolves to your function's value alone and DISCARDS what
101
+ * Decidio recorded. Kept because a caller who truly does not care should not have to
102
+ * destructure — but "the report was not recorded" and "the execution could not be attested"
103
+ * become invisible again, which is the gap 0.2.0 exists to close. Prefer `protect`. */
104
+ protectBestEffort<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
88
105
  }
89
106
  /** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
90
107
  export declare const guard: Guard;
package/dist/guard.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { withApproval } from "./index.js";
2
2
  import { createDecidioResume, FilePendingStore } from "./resume.js";
3
- import { join } from "node:path";
4
3
  export class Guard {
5
4
  config;
6
5
  _resume;
@@ -38,7 +37,6 @@ export class Guard {
38
37
  this._resume = createDecidioResume({
39
38
  apiUrl: c.apiUrl, apiToken: c.apiToken, webhookSecret: c.webhookSecret,
40
39
  resumeUrl: c.resumeUrl, store: new FilePendingStore(c.pendingDir ?? ".decidio-pending"),
41
- reportDir: join(c.pendingDir ?? ".decidio-pending", "reports"),
42
40
  agentId: c.agentId, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId,
43
41
  fetchImpl: c.fetchImpl,
44
42
  });
@@ -70,12 +68,16 @@ export class Guard {
70
68
  `(pollTimeoutMs), and when it does, the decision stays OPEN in Decidio while this process stops waiting: ` +
71
69
  `a later approval cannot resume it. Only safe for short, watched approvals — omit mode:"blocking" for ` +
72
70
  `durable async resume on real (minutes-to-days) approvals.`);
73
- return withApproval(fn, describe, base, opts?.evidence ? { evidence: opts.evidence } : undefined);
71
+ return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
74
72
  }
75
73
  const resume = this.ensureResume();
76
74
  if (opts?.action)
77
- resume.register(opts.action, fn, opts?.evidence);
78
- return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, opts?.evidence ? { evidence: opts.evidence } : undefined);
75
+ resume.register(opts.action, fn, opts?.evidence, opts?.withContext);
76
+ return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
77
+ }
78
+ protectBestEffort(fn, describe, opts) {
79
+ const wrapped = this.protect(fn, describe, opts);
80
+ return async (...a) => (await wrapped(...a)).value;
79
81
  }
80
82
  }
81
83
  export const guard = Guard.fromEnv();
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { ResumeController } from "./resume.js";
2
+ import { type EvidenceOutcome } from "./evidence.js";
2
3
  export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
3
4
  export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
4
5
  export { guard, Guard } from "./guard.js";
5
6
  export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
6
- export { DecidioHttpError } from "./errors.js";
7
+ export { DecidioHttpError, decisionIdProblem } from "./errors.js";
7
8
  export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
8
9
  export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
9
10
  export type { JsonValue, JsonPrimitive } from "./canonical.js";
@@ -107,10 +108,34 @@ export declare class DecidioNetworkError extends Error {
107
108
  /** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
108
109
  * the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
109
110
  export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
111
+ /** What a protected call returns in 0.2.0: your function's value, the decision it was authorized
112
+ * under, and what Decidio actually recorded about the execution.
113
+ *
114
+ * Through 0.1.x this was just `R`. The SDK computed the full confirmation outcome — tier, whether
115
+ * the report was recorded, whether the signed execution report verified, whether the evidence
116
+ * mapper failed — and then dropped it on the floor, surfacing it only as a console warning. For a
117
+ * product whose value IS the record, the one thing a caller could not do was find out what got
118
+ * recorded. Returning it makes "my action ran but was logged as an unproven assertion" something
119
+ * code can branch on instead of something a human might notice in a log.
120
+ *
121
+ * `protectBestEffort` keeps the old value-only shape for callers that genuinely do not care. */
122
+ export interface ProtectedExecution<R> {
123
+ /** Exactly what your function returned — unchanged, uncanonicalized, your object. */
124
+ value: R;
125
+ /** The decision this execution was authorized under. Use it as your downstream idempotency key. */
126
+ decisionId: string;
127
+ /** What Decidio recorded. `reportRecorded` false means the execution happened and Decidio does
128
+ * NOT know; `executionConfirmed` false means it knows but could not attest a commit. */
129
+ confirmation: EvidenceOutcome;
130
+ }
110
131
  export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig, opts?: {
111
132
  /** Map YOUR function's natural return value to the evidence Decidio records. The gate awards
112
133
  * its strongest wrapper tier (application_confirmed) only for a committed record id plus a
113
134
  * success indication — this lets a real API keep its own shape instead of being forced into
114
135
  * magic field names: `evidence: (r) => ({ recordId: r.transactionId, success: r.status === "committed" })`. */
115
136
  evidence?: (result: R) => unknown;
116
- }): (...args: A) => Promise<R>;
137
+ /** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
138
+ * idempotencyKey }`. Opt-in, never inferred from the function's arity — guessing wrong means
139
+ * passing a context object as someone's first business argument. */
140
+ withContext?: boolean;
141
+ }): (...args: A) => Promise<ProtectedExecution<R>>;
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@ import { canonicalize, canonicalizeArgs } from "./canonical.js";
4
4
  import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
5
5
  export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
6
6
  export { guard, Guard } from "./guard.js";
7
- export { DecidioHttpError } from "./errors.js";
8
- import { DecidioHttpError } from "./errors.js";
7
+ export { DecidioHttpError, decisionIdProblem } from "./errors.js";
8
+ import { DecidioHttpError, decisionIdProblem } from "./errors.js";
9
9
  export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
10
10
  export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
11
11
  export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
@@ -142,6 +142,12 @@ export function withApproval(fn, describe, config, opts) {
142
142
  ? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
143
143
  : undefined;
144
144
  canonicalize(ctx, "describe(...)");
145
+ if (typeof ctx?.action !== "string" || ctx.action.trim().length === 0) {
146
+ throw new Error(`[decidio] describe(...) must return an \`action\` string naming what is being asked (got ${JSON.stringify(ctx?.action ?? null)}). Nothing was routed or executed.`);
147
+ }
148
+ if (typeof ctx.amount !== "number" || !Number.isFinite(ctx.amount) || ctx.amount < 0) {
149
+ throw new Error(`[decidio] describe(...) must return a finite, non-negative \`amount\` — the policy decides on it, so a request without one is not a request Decidio can rule on (got ${JSON.stringify(ctx.amount ?? null)}). Use 0 for an action with no monetary value. Nothing was routed or executed.`);
150
+ }
145
151
  const auth = await post(config, "/agent/authorize", {
146
152
  requester: config.agentId, action: ctx.action, amount: ctx.amount,
147
153
  scope: ctx.scope, sourceSystem: config.sourceSystem,
@@ -151,8 +157,9 @@ export function withApproval(fn, describe, config, opts) {
151
157
  });
152
158
  if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
153
159
  throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
154
- if (typeof auth.decisionId !== "string" || !auth.decisionId)
155
- throw new DecidioBlockedError(`the gate answered "${auth.decision}" with no usable decisionId (${JSON.stringify(auth.decisionId ?? null)}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, auth.decisionId);
160
+ const idProblem = decisionIdProblem(auth.decisionId);
161
+ if (idProblem)
162
+ throw new DecidioBlockedError(`the gate answered "${auth.decision}" with no usable decisionId (${JSON.stringify(auth.decisionId ?? null)} — ${idProblem}) — refusing to execute. Every authorized action must be addressable: without an id it cannot be polled, resumed, confirmed or audited.`, typeof auth.decisionId === "string" ? auth.decisionId : String(auth.decisionId ?? ""));
156
163
  if (auth.decision === "block")
157
164
  throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
158
165
  if (auth.decision === "route") {
@@ -188,7 +195,9 @@ export function withApproval(fn, describe, config, opts) {
188
195
  await sleep(pollInterval);
189
196
  }
190
197
  }
191
- const result = await fn(...args);
198
+ const result = opts?.withContext
199
+ ? await fn({ decisionId: auth.decisionId, attemptId: null, idempotencyKey: auth.decisionId }, ...args)
200
+ : await fn(...args);
192
201
  let reported = null;
193
202
  let evidenceProblem;
194
203
  try {
@@ -199,20 +208,31 @@ export function withApproval(fn, describe, config, opts) {
199
208
  evidenceProblem = String(e?.message ?? e);
200
209
  console.warn(`[decidio] ${auth.decisionId}: the action SUCCEEDED but its result could not be reported as evidence (${evidenceProblem}). Reporting an empty captured response so the execution is still recorded — map the return value with the \`evidence\` option (e.g. evidence: (r) => ({ recordId: String(r.id), success: true })) to earn a confirming tier. The action was NOT re-run.`);
201
210
  }
211
+ let confirmation;
202
212
  try {
203
213
  const confirmIdentity = signConfirm(auth.decisionId, config);
204
214
  const confirmBody = await post(config, "/agent/confirm", buildConfirmBody(config, auth.decisionId, reported, confirmIdentity));
205
- const ev = readConfirmResponse(confirmBody, { signed: !!confirmIdentity, capturedResponse: reported });
206
- if (!ev.reportRecorded)
207
- console.warn(`[decidio] ${auth.decisionId}: ${ev.diagnostic}`);
208
- else if (!ev.executionConfirmed || ev.executionReportVerified === false)
209
- console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${ev.evidenceTier} — ${ev.diagnostic}`);
215
+ confirmation = readConfirmResponse(confirmBody, { signed: !!confirmIdentity, capturedResponse: reported, journalled: false });
216
+ if (!confirmation.reportRecorded)
217
+ console.warn(`[decidio] ${auth.decisionId}: ${confirmation.diagnostic}`);
218
+ else if (!confirmation.executionConfirmed || confirmation.executionReportVerified === false)
219
+ console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as ${confirmation.evidenceTier} — ${confirmation.diagnostic}`);
210
220
  if (evidenceProblem)
211
221
  console.warn(`[decidio] ${auth.decisionId}: the execution IS recorded, but with an empty captured response — see the evidence note above.`);
212
222
  }
213
223
  catch (e) {
214
- console.warn(`[decidio] confirmation post failed (action succeeded): ${e.message}`);
224
+ const isHttp = e instanceof DecidioHttpError;
225
+ console.warn(`[decidio] confirmation post failed (action succeeded): ${e?.message ?? e}`);
226
+ confirmation = {
227
+ reportRecorded: false,
228
+ executionConfirmed: false,
229
+ confirmationRetryable: isHttp ? e.retryable : true,
230
+ reportJournalled: false,
231
+ diagnostic: `your action RAN and Decidio does NOT have its execution report (${e?.message ?? e}). This path journals nothing, so there is no local copy to re-send: record the outcome from your own system. Durable mode DOES journal reports, and its recovery path can re-send them — that is the mode to use if this matters to you. Do NOT re-run the action.`,
232
+ };
215
233
  }
216
- return result;
234
+ if (evidenceProblem)
235
+ confirmation.evidenceError = evidenceProblem;
236
+ return { value: result, decisionId: auth.decisionId, confirmation };
217
237
  };
218
238
  }
package/dist/inngest.d.ts CHANGED
@@ -45,6 +45,10 @@ export declare function decidioGateStep<R>(args: {
45
45
  ctx: ApprovalContext;
46
46
  run: () => Promise<R>;
47
47
  timeout?: string;
48
+ /** Map your action's return value to the evidence Decidio records — the same option the core
49
+ * path takes. This door's own confirm chokepoint tells callers to use it; until 0.2.0 no
50
+ * adapter door accepted one, so the advice was unfollowable everywhere it was printed. */
51
+ evidence?: (result: R) => unknown;
48
52
  }): Promise<GateStepOutcome<R>>;
49
53
  /**
50
54
  * Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
package/dist/inngest.js CHANGED
@@ -22,7 +22,7 @@ export async function decidioGateStep(args) {
22
22
  return { status: "blocked", decisionId: auth.decisionId };
23
23
  }
24
24
  const result = await step.run("agent-action", args.run);
25
- await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result); return true; });
25
+ await step.run("decidio-confirm", async () => { await gateConfirm(config, auth.decisionId, result, args.evidence); return true; });
26
26
  return { status: "executed", decisionId: auth.decisionId, result };
27
27
  }
28
28
  export function createInngestResumeBridge(opts) {