@spendguard/sdk 0.5.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 +190 -0
- package/LICENSE_NOTICES.md +127 -0
- package/README.md +151 -0
- package/dist/adapter-D9T3yEEw.d.ts +3441 -0
- package/dist/cache-DOnw8QtJ.d.ts +1164 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +74 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +4815 -0
- package/dist/errors.d.ts +269 -0
- package/dist/errors.js +148 -0
- package/dist/ids.d.ts +69 -0
- package/dist/ids.js +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +5295 -0
- package/dist/otel.d.ts +118 -0
- package/dist/otel.js +84 -0
- package/dist/pricing/demo.d.ts +26 -0
- package/dist/pricing/demo.js +138 -0
- package/dist/pricing.d.ts +70 -0
- package/dist/pricing.js +92 -0
- package/dist/promptHash.d.ts +23 -0
- package/dist/promptHash.js +25 -0
- package/dist/proto.d.ts +609 -0
- package/dist/proto.js +3055 -0
- package/dist/retry.d.ts +121 -0
- package/dist/retry.js +92 -0
- package/dist/runPlan.d.ts +69 -0
- package/dist/runPlan.js +35 -0
- package/fixtures/cross-language/v1.json +327 -0
- package/package.json +123 -0
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification bucket for an RPC error per Python `_classify_rpc_error`.
|
|
3
|
+
*
|
|
4
|
+
* - `"transient"` → the sidecar / network is temporarily unavailable. Safe to
|
|
5
|
+
* retry IF the caller has a stable idempotency key.
|
|
6
|
+
* - `"permanent"` → all other failures (invalid argument, decision denied,
|
|
7
|
+
* precondition failed, etc.). MUST NOT retry — retrying a permanent error
|
|
8
|
+
* either does nothing (waste of latency budget) or worse, re-asserts a
|
|
9
|
+
* broken request shape.
|
|
10
|
+
*/
|
|
11
|
+
type RpcErrorClassification = "transient" | "permanent";
|
|
12
|
+
/**
|
|
13
|
+
* The gRPC status codes that mirror Python's transient-error bucket.
|
|
14
|
+
* Listed here as a frozen set so tests can assert the bucket is the locked
|
|
15
|
+
* three (review-standards.md §1.2 verbatim-signature gate).
|
|
16
|
+
*
|
|
17
|
+
* The string values are the canonical `@grpc/grpc-js` status names; same
|
|
18
|
+
* spelling as `grpc.StatusCode.<NAME>` and `protobuf-ts`'s `RpcError.code`.
|
|
19
|
+
*/
|
|
20
|
+
declare const TRANSIENT_STATUS_CODES: ReadonlySet<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Classify an error as transient or permanent.
|
|
23
|
+
*
|
|
24
|
+
* Inputs accepted (in order of dispatch):
|
|
25
|
+
* 1. `SidecarUnavailable` instance — `"transient"` (we already classified it
|
|
26
|
+
* upstream via `mapGrpcStatusToError`).
|
|
27
|
+
* 2. Object with `.code` matching one of `TRANSIENT_STATUS_CODES` — typed
|
|
28
|
+
* `"transient"`. This is the `protobuf-ts` `RpcError` shape AND the
|
|
29
|
+
* `@grpc/grpc-js` ServiceError shape AND a duck-typed mock for tests.
|
|
30
|
+
* 3. Everything else — `"permanent"`.
|
|
31
|
+
*
|
|
32
|
+
* Matches Python `_classify_rpc_error` line-for-line: same three statuses,
|
|
33
|
+
* same fallthrough, same conservative default ("if we don't know, don't retry").
|
|
34
|
+
*
|
|
35
|
+
* @param err The error to classify. Accepts any thrown value (RpcError,
|
|
36
|
+
* SpendGuardError subclasses, plain Error, string, undefined, …).
|
|
37
|
+
* @returns `"transient"` or `"permanent"`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* import { classifyRpcError } from "@spendguard/sdk/retry";
|
|
41
|
+
*
|
|
42
|
+
* try {
|
|
43
|
+
* await client.reserve(req);
|
|
44
|
+
* } catch (err) {
|
|
45
|
+
* if (classifyRpcError(err) === "transient") {
|
|
46
|
+
* // queue for retry
|
|
47
|
+
* } else {
|
|
48
|
+
* throw err;
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
declare function classifyRpcError(err: unknown): RpcErrorClassification;
|
|
53
|
+
/**
|
|
54
|
+
* Options for `runWithRetry`.
|
|
55
|
+
*
|
|
56
|
+
* - `idempotencyKey` — REQUIRED for any retry to actually occur. Without one,
|
|
57
|
+
* `runWithRetry` runs `fn` exactly once and rethrows a pointed
|
|
58
|
+
* `SidecarUnavailable(cause: err)` on a transient error so the adapter can
|
|
59
|
+
* route. With one, `fn` runs up to `maxAttempts` times on transient errors.
|
|
60
|
+
* - `maxAttempts` — total attempts (initial + retries). Default 2; legal
|
|
61
|
+
* range [1, 5]. The default 2 matches design.md §6.5 line 428 ("initial +
|
|
62
|
+
* 1 retry"); values ≥ 5 require an explicit opt-in to discourage runaway
|
|
63
|
+
* retry loops in the substrate.
|
|
64
|
+
* - `baseBackoffMs` — fixed delay between attempts. Default 25 ms per
|
|
65
|
+
* design.md §6.5 line 429.
|
|
66
|
+
* - `jitterMs` — random delay added on top of `baseBackoffMs`. Default 25 ms;
|
|
67
|
+
* sampled uniformly from `[0, jitterMs]`. Avoids retry-storm pile-ups when
|
|
68
|
+
* N adapters share a single sidecar.
|
|
69
|
+
* - `sleep` — pluggable sleeper for tests. Defaults to `setTimeout`.
|
|
70
|
+
*/
|
|
71
|
+
interface RunWithRetryOptions {
|
|
72
|
+
idempotencyKey?: string;
|
|
73
|
+
maxAttempts?: number;
|
|
74
|
+
baseBackoffMs?: number;
|
|
75
|
+
jitterMs?: number;
|
|
76
|
+
sleep?: (ms: number) => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Run `fn()` with bounded retry per design.md §6.5 + Python
|
|
80
|
+
* `_classify_rpc_error` parity.
|
|
81
|
+
*
|
|
82
|
+
* Algorithm:
|
|
83
|
+
* 1. Run `fn()`. If it succeeds, return the value.
|
|
84
|
+
* 2. If it throws, classify the error via `classifyRpcError(err)`.
|
|
85
|
+
* - PERMANENT → rethrow as-is (NEVER retry).
|
|
86
|
+
* - TRANSIENT without `idempotencyKey` → throw
|
|
87
|
+
* `SidecarUnavailable(cause: err)` immediately. Caller's adapter is
|
|
88
|
+
* responsible for routing; the substrate refuses to retry without a
|
|
89
|
+
* stable key because a fresh decision on retry would double-reserve.
|
|
90
|
+
* - TRANSIENT with `idempotencyKey` AND attempts remaining → sleep
|
|
91
|
+
* `baseBackoffMs + uniform(0, jitterMs)`, then go to step 1.
|
|
92
|
+
* - TRANSIENT with `idempotencyKey` AND attempts exhausted → throw the
|
|
93
|
+
* LAST error (preserves the original `cause` if it's already a
|
|
94
|
+
* `SidecarUnavailable`).
|
|
95
|
+
*
|
|
96
|
+
* Permanent errors NEVER retry — design.md §6.5 line 426 spells out the
|
|
97
|
+
* three-status transient cluster as the only retry-eligible surface.
|
|
98
|
+
*
|
|
99
|
+
* The `sleep` function in tests defaults to `setTimeout` — for unit tests,
|
|
100
|
+
* pass a no-op (`async () => {}`) so the retry loop runs synchronously.
|
|
101
|
+
*
|
|
102
|
+
* @param fn The function to run. Awaited; thrown errors are inspected.
|
|
103
|
+
* @param opts Retry options. `idempotencyKey` MUST be set for retries to
|
|
104
|
+
* actually occur.
|
|
105
|
+
*
|
|
106
|
+
* @returns The result of `fn()` on success.
|
|
107
|
+
*
|
|
108
|
+
* @throws SidecarUnavailable when `fn` fails transiently without an
|
|
109
|
+
* idempotency key.
|
|
110
|
+
* @throws The last error from `fn` when attempts are exhausted (or the
|
|
111
|
+
* error type is permanent).
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* await runWithRetry(
|
|
115
|
+
* () => client.requestDecision(grpcReq, { timeout: 250 }),
|
|
116
|
+
* { idempotencyKey: req.idempotencyKey, maxAttempts: 2 },
|
|
117
|
+
* );
|
|
118
|
+
*/
|
|
119
|
+
declare function runWithRetry<T>(fn: () => Promise<T>, opts?: RunWithRetryOptions): Promise<T>;
|
|
120
|
+
|
|
121
|
+
export { type RpcErrorClassification, type RunWithRetryOptions, TRANSIENT_STATUS_CODES, classifyRpcError, runWithRetry };
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SpendGuardError = class extends Error {
|
|
3
|
+
name = "SpendGuardError";
|
|
4
|
+
constructor(message, opts) {
|
|
5
|
+
super(message);
|
|
6
|
+
if (opts?.cause !== void 0) {
|
|
7
|
+
this.cause = opts.cause;
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(this, "name", {
|
|
10
|
+
value: this.name,
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var SidecarUnavailable = class extends SpendGuardError {
|
|
18
|
+
name = "SidecarUnavailable";
|
|
19
|
+
statusCode = 503;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/retry.ts
|
|
23
|
+
var TRANSIENT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
24
|
+
"UNAVAILABLE",
|
|
25
|
+
"DEADLINE_EXCEEDED",
|
|
26
|
+
"CANCELLED"
|
|
27
|
+
]);
|
|
28
|
+
function classifyRpcError(err) {
|
|
29
|
+
if (err instanceof SidecarUnavailable) return "transient";
|
|
30
|
+
if (err !== null && typeof err === "object" && "code" in err) {
|
|
31
|
+
const code = err.code;
|
|
32
|
+
if (typeof code === "string" && TRANSIENT_STATUS_CODES.has(code)) {
|
|
33
|
+
return "transient";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "permanent";
|
|
37
|
+
}
|
|
38
|
+
async function runWithRetry(fn, opts = {}) {
|
|
39
|
+
const idempotencyKey = opts.idempotencyKey;
|
|
40
|
+
const maxAttempts = clampMaxAttempts(opts.maxAttempts ?? 2);
|
|
41
|
+
const baseBackoffMs = opts.baseBackoffMs ?? 25;
|
|
42
|
+
const jitterMs = opts.jitterMs ?? 25;
|
|
43
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
44
|
+
let lastErr;
|
|
45
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const classification = classifyRpcError(err);
|
|
50
|
+
if (classification === "permanent") {
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
if (idempotencyKey === void 0 || idempotencyKey.length === 0) {
|
|
54
|
+
if (err instanceof SidecarUnavailable) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
throw new SidecarUnavailable(
|
|
58
|
+
`transient RPC failure with no idempotency key; refusing to retry: ${errorMessage(err)}`,
|
|
59
|
+
{ cause: err }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
lastErr = err;
|
|
63
|
+
if (attempt < maxAttempts) {
|
|
64
|
+
const delay = baseBackoffMs + Math.floor(Math.random() * (jitterMs + 1));
|
|
65
|
+
await sleep(delay);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw lastErr ?? new SpendGuardError("runWithRetry: unreachable");
|
|
72
|
+
}
|
|
73
|
+
function clampMaxAttempts(value) {
|
|
74
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) return 2;
|
|
75
|
+
if (value < 1) return 1;
|
|
76
|
+
if (value > 5) return 5;
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function defaultSleep(ms) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
setTimeout(resolve, ms);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function errorMessage(err) {
|
|
85
|
+
if (err instanceof Error) return err.message;
|
|
86
|
+
if (typeof err === "string") return err;
|
|
87
|
+
return String(err);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { TRANSIENT_STATUS_CODES, classifyRpcError, runWithRetry };
|
|
91
|
+
//# sourceMappingURL=retry.js.map
|
|
92
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-declared plan for one logical agent run.
|
|
3
|
+
*
|
|
4
|
+
* - `plannedCalls` — expected number of LLM calls in the run. Non-negative integer.
|
|
5
|
+
* - `plannedTools` — expected number of tool calls in the run. Non-negative integer.
|
|
6
|
+
*
|
|
7
|
+
* The SDK ships `plannedStepsHint = plannedCalls + plannedTools` on every
|
|
8
|
+
* `DecisionRequest` issued inside the active scope (per spec §5.1: steps are
|
|
9
|
+
* the disjoint union of LLM + tool calls). The sidecar-side projector
|
|
10
|
+
* enforces an upper bound `[0, MAX_PLANNED_STEPS]`
|
|
11
|
+
* (`services/run_cost_projector/src/server.rs::MAX_PLANNED_STEPS`); we don't
|
|
12
|
+
* repeat the bound here so a future Rust-side bump doesn't require an SDK
|
|
13
|
+
* release.
|
|
14
|
+
*/
|
|
15
|
+
interface RunPlan {
|
|
16
|
+
plannedCalls: number;
|
|
17
|
+
plannedTools: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read the in-scope `RunPlan` if one exists, otherwise `null`.
|
|
21
|
+
*
|
|
22
|
+
* Returns `null` (not `undefined`) per LOCKED design.md §4.7 line 300 — the
|
|
23
|
+
* shape is `RunPlan | null` for parity with the Python `current_run_plan()`
|
|
24
|
+
* reference (which returns `None`) and review-standards §8.3.
|
|
25
|
+
*
|
|
26
|
+
* Returned object is the SAME reference `withRunPlan` stored (defensively
|
|
27
|
+
* copied at scope entry); adapters MUST treat the returned value as
|
|
28
|
+
* immutable.
|
|
29
|
+
*/
|
|
30
|
+
declare function currentRunPlan(): RunPlan | null;
|
|
31
|
+
/**
|
|
32
|
+
* Higher-order function that installs `plan` in async-local scope around `fn`.
|
|
33
|
+
*
|
|
34
|
+
* CURRIED form per design.md §4.7 lines 295-298 — calling `withRunPlan(plan, fn)`
|
|
35
|
+
* returns a NEW callable; the wrapped `fn` is only invoked when the returned
|
|
36
|
+
* callable is called. Mirrors the `@with_run_plan(...)` decorator pattern in
|
|
37
|
+
* `sdk/python/src/spendguard/run_plan.py`.
|
|
38
|
+
*
|
|
39
|
+
* ## Validation (LOCKED §8.4)
|
|
40
|
+
*
|
|
41
|
+
* `plannedCalls` and `plannedTools` are validated at HOF construction time
|
|
42
|
+
* (NOT at wrapped-call time): if either is missing, non-integer, or negative,
|
|
43
|
+
* a `TypeError` is thrown synchronously from `withRunPlan` — surfacing the
|
|
44
|
+
* misuse at decorator application rather than first call. `plannedTools` is
|
|
45
|
+
* optional on input and defaults to `0`.
|
|
46
|
+
*
|
|
47
|
+
* ## Nesting (LOCKED §8.2 — OUTER WINS)
|
|
48
|
+
*
|
|
49
|
+
* When the returned callable is invoked inside an existing `withRunPlan`
|
|
50
|
+
* scope, the inner call is a NO-OP for plan storage — the outer plan stays
|
|
51
|
+
* active and `currentRunPlan()` continues to return it. The inner `fn` is
|
|
52
|
+
* still invoked with its arguments; only the storage swap is skipped. This
|
|
53
|
+
* matches `run_plan.py` lines 183-191 (outer wins) and protects the budget
|
|
54
|
+
* envelope from being silently rewritten by a sub-agent helper.
|
|
55
|
+
*
|
|
56
|
+
* @param plan The run plan to install. Validated immediately.
|
|
57
|
+
* @param fn The (sync or async) function to run inside the plan scope.
|
|
58
|
+
* @returns A NEW async function that, when called, runs `fn(...args)` with
|
|
59
|
+
* the plan in scope and returns `Promise<TRet>`.
|
|
60
|
+
*
|
|
61
|
+
* @throws TypeError if `plannedCalls` or `plannedTools` is not a non-negative
|
|
62
|
+
* integer.
|
|
63
|
+
*/
|
|
64
|
+
declare function withRunPlan<TArgs extends unknown[], TRet>(plan: {
|
|
65
|
+
plannedCalls: number;
|
|
66
|
+
plannedTools?: number;
|
|
67
|
+
}, fn: (...args: TArgs) => TRet | Promise<TRet>): (...args: TArgs) => Promise<TRet>;
|
|
68
|
+
|
|
69
|
+
export { type RunPlan, currentRunPlan, withRunPlan };
|
package/dist/runPlan.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
// src/runPlan.ts
|
|
4
|
+
var storage = new AsyncLocalStorage();
|
|
5
|
+
function currentRunPlan() {
|
|
6
|
+
return storage.getStore() ?? null;
|
|
7
|
+
}
|
|
8
|
+
function withRunPlan(plan, fn) {
|
|
9
|
+
if (!Number.isInteger(plan.plannedCalls) || plan.plannedCalls < 0) {
|
|
10
|
+
throw new TypeError(
|
|
11
|
+
`withRunPlan: plannedCalls must be a non-negative integer, got ${String(plan.plannedCalls)}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
const tools = plan.plannedTools ?? 0;
|
|
15
|
+
if (!Number.isInteger(tools) || tools < 0) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`withRunPlan: plannedTools must be a non-negative integer, got ${String(plan.plannedTools)}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const fullPlan = Object.freeze({
|
|
21
|
+
plannedCalls: plan.plannedCalls,
|
|
22
|
+
plannedTools: tools
|
|
23
|
+
});
|
|
24
|
+
return async (...args) => {
|
|
25
|
+
const existing = storage.getStore();
|
|
26
|
+
if (existing !== void 0) {
|
|
27
|
+
return await fn(...args);
|
|
28
|
+
}
|
|
29
|
+
return await storage.run(fullPlan, () => fn(...args));
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { currentRunPlan, withRunPlan };
|
|
34
|
+
//# sourceMappingURL=runPlan.js.map
|
|
35
|
+
//# sourceMappingURL=runPlan.js.map
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"generated_at": "2026-06-07",
|
|
4
|
+
"generated_with": {
|
|
5
|
+
"python_reference": "spendguard.ids.* + spendguard.prompt_hash.compute",
|
|
6
|
+
"note": "Python implementation is the reference. TS asserts the SAME expected_output for the SAME inputs. Drift in either direction is a P0 review-standards §2 blocker."
|
|
7
|
+
},
|
|
8
|
+
"fixtures": [
|
|
9
|
+
{
|
|
10
|
+
"id": "FX1",
|
|
11
|
+
"fn": "derive_idempotency_key",
|
|
12
|
+
"description": "ASCII numeric IDs, LLM_CALL_PRE trigger",
|
|
13
|
+
"inputs": {
|
|
14
|
+
"tenant_id": "t-1",
|
|
15
|
+
"session_id": "s-1",
|
|
16
|
+
"run_id": "r-1",
|
|
17
|
+
"step_id": "step-1",
|
|
18
|
+
"llm_call_id": "llm-1",
|
|
19
|
+
"trigger": "LLM_CALL_PRE"
|
|
20
|
+
},
|
|
21
|
+
"expected_output": "sg-df6a372619ee74530c2d9e6e4cbbc4b9"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "FX2",
|
|
25
|
+
"fn": "derive_idempotency_key",
|
|
26
|
+
"description": "ASCII numeric IDs, alternate values",
|
|
27
|
+
"inputs": {
|
|
28
|
+
"tenant_id": "t-2",
|
|
29
|
+
"session_id": "s-2",
|
|
30
|
+
"run_id": "r-2",
|
|
31
|
+
"step_id": "step-2",
|
|
32
|
+
"llm_call_id": "llm-2",
|
|
33
|
+
"trigger": "LLM_CALL_PRE"
|
|
34
|
+
},
|
|
35
|
+
"expected_output": "sg-faefca72f21e98f85ca1428a07ff74cf"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "FX3",
|
|
39
|
+
"fn": "derive_idempotency_key",
|
|
40
|
+
"description": "Canonical UUID tenant",
|
|
41
|
+
"inputs": {
|
|
42
|
+
"tenant_id": "00000000-0000-0000-0000-000000000001",
|
|
43
|
+
"session_id": "sess-1",
|
|
44
|
+
"run_id": "run-1",
|
|
45
|
+
"step_id": "step-1",
|
|
46
|
+
"llm_call_id": "llm-1",
|
|
47
|
+
"trigger": "LLM_CALL_PRE"
|
|
48
|
+
},
|
|
49
|
+
"expected_output": "sg-8f58c05cb80e39934ac161f5a4c7db40"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "FX4",
|
|
53
|
+
"fn": "derive_idempotency_key",
|
|
54
|
+
"description": "Empty trigger field",
|
|
55
|
+
"inputs": {
|
|
56
|
+
"tenant_id": "tenant-abc",
|
|
57
|
+
"session_id": "sess-1",
|
|
58
|
+
"run_id": "run-1",
|
|
59
|
+
"step_id": "step-1",
|
|
60
|
+
"llm_call_id": "llm-1",
|
|
61
|
+
"trigger": ""
|
|
62
|
+
},
|
|
63
|
+
"expected_output": "sg-3a2090b41777421828f4362daca7aaa5"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "FX5",
|
|
67
|
+
"fn": "derive_idempotency_key",
|
|
68
|
+
"description": "All fields empty",
|
|
69
|
+
"inputs": {
|
|
70
|
+
"tenant_id": "",
|
|
71
|
+
"session_id": "",
|
|
72
|
+
"run_id": "",
|
|
73
|
+
"step_id": "",
|
|
74
|
+
"llm_call_id": "",
|
|
75
|
+
"trigger": ""
|
|
76
|
+
},
|
|
77
|
+
"expected_output": "sg-ff302dc3560c000bc0cf9b9f72359b10"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "FX6",
|
|
81
|
+
"fn": "derive_idempotency_key",
|
|
82
|
+
"description": "AGENT_STEP_PRE trigger boundary",
|
|
83
|
+
"inputs": {
|
|
84
|
+
"tenant_id": "tenant-xyz",
|
|
85
|
+
"session_id": "sess-42",
|
|
86
|
+
"run_id": "run-42",
|
|
87
|
+
"step_id": "step-7",
|
|
88
|
+
"llm_call_id": "llm-7",
|
|
89
|
+
"trigger": "AGENT_STEP_PRE"
|
|
90
|
+
},
|
|
91
|
+
"expected_output": "sg-a0c3ef27b9ef67c2649e3f54763f28cd"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "FX7",
|
|
95
|
+
"fn": "derive_idempotency_key",
|
|
96
|
+
"description": "Multi-byte UTF-8 tenant id (CJK)",
|
|
97
|
+
"inputs": {
|
|
98
|
+
"tenant_id": "租户-甲",
|
|
99
|
+
"session_id": "sess-1",
|
|
100
|
+
"run_id": "run-1",
|
|
101
|
+
"step_id": "step-1",
|
|
102
|
+
"llm_call_id": "llm-1",
|
|
103
|
+
"trigger": "LLM_CALL_PRE"
|
|
104
|
+
},
|
|
105
|
+
"expected_output": "sg-95f41144d9bcd120386eeba22b83b74c"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "FX8",
|
|
109
|
+
"fn": "derive_idempotency_key",
|
|
110
|
+
"description": "Unit-Separator probe (long tenant, empty session)",
|
|
111
|
+
"inputs": {
|
|
112
|
+
"tenant_id": "abcd",
|
|
113
|
+
"session_id": "",
|
|
114
|
+
"run_id": "x",
|
|
115
|
+
"step_id": "y",
|
|
116
|
+
"llm_call_id": "z",
|
|
117
|
+
"trigger": "T"
|
|
118
|
+
},
|
|
119
|
+
"expected_output": "sg-28cc1690aed794c64279c30f8ba49dfb"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "FXP1",
|
|
123
|
+
"fn": "compute_prompt_hash",
|
|
124
|
+
"description": "ASCII prompt + canonical UUID tenant",
|
|
125
|
+
"inputs": {
|
|
126
|
+
"prompt_text": "hello world",
|
|
127
|
+
"tenant_id": "00000000-0000-0000-0000-000000000001"
|
|
128
|
+
},
|
|
129
|
+
"expected_output": "5d55a1ebc9782455de0979780fd6cf686127dadcba580f230ddc3fea31516d0d"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "FXP2",
|
|
133
|
+
"fn": "compute_prompt_hash",
|
|
134
|
+
"description": "Empty prompt + canonical UUID tenant",
|
|
135
|
+
"inputs": {
|
|
136
|
+
"prompt_text": "",
|
|
137
|
+
"tenant_id": "00000000-0000-0000-0000-000000000001"
|
|
138
|
+
},
|
|
139
|
+
"expected_output": "698e521970ba6005a5555a4dc63797488a0f673a3386adfe0410aa11c9b6757b"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "FXP3",
|
|
143
|
+
"fn": "compute_prompt_hash",
|
|
144
|
+
"description": "Whitespace-padded prompt + non-UUID tenant (strip gate)",
|
|
145
|
+
"inputs": {
|
|
146
|
+
"prompt_text": " trim me ",
|
|
147
|
+
"tenant_id": "tenant-abc"
|
|
148
|
+
},
|
|
149
|
+
"expected_output": "d97fa1377ce7133eeae08a7b9d67eaf50f80edc917d4550720ac6e9fccbd89e4"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "FXP4",
|
|
153
|
+
"fn": "compute_prompt_hash",
|
|
154
|
+
"description": "Multi-byte UTF-8 prompt (CJK punctuation)",
|
|
155
|
+
"inputs": {
|
|
156
|
+
"prompt_text": "Hello, 世界!",
|
|
157
|
+
"tenant_id": "00000000-0000-0000-0000-000000000042"
|
|
158
|
+
},
|
|
159
|
+
"expected_output": "7caa95cf8b5b9118721f192d4998515655d570c40be02c4c8402a201c6e2f7e5"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"id": "FXP5",
|
|
163
|
+
"fn": "compute_prompt_hash",
|
|
164
|
+
"description": "UTF-8 BOM-prefixed prompt (non-ASCII whitespace preserved)",
|
|
165
|
+
"inputs": {
|
|
166
|
+
"prompt_text": "test prompt",
|
|
167
|
+
"tenant_id": "00000000-0000-0000-0000-000000000007"
|
|
168
|
+
},
|
|
169
|
+
"expected_output": "83476ff5befaf561d2b1b72b8dd803f2dce55219abb0041ee283cd451e6e8d66"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "FXP6",
|
|
173
|
+
"fn": "compute_prompt_hash",
|
|
174
|
+
"description": "Embedded control characters NUL/BEL/VT preserved",
|
|
175
|
+
"inputs": {
|
|
176
|
+
"prompt_text": "before\u0000\u0007\u000bafter",
|
|
177
|
+
"tenant_id": "tenant-control"
|
|
178
|
+
},
|
|
179
|
+
"expected_output": "25274faa082e3f462b3956657b1e7f4624f4210f863b755e497349693013f8c7"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "FXP7",
|
|
183
|
+
"fn": "compute_prompt_hash",
|
|
184
|
+
"description": "10KB ASCII prompt + non-UUID tenant",
|
|
185
|
+
"inputs": {
|
|
186
|
+
"prompt_text": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
187
|
+
"tenant_id": "tenant-long"
|
|
188
|
+
},
|
|
189
|
+
"expected_output": "b4c8c062972c201681f906fbb86560d6304628bf0664d5375929cbc9379fa88d"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": "FXP8",
|
|
193
|
+
"fn": "compute_prompt_hash",
|
|
194
|
+
"description": "Mixed-case UUID tenant (canonicaliser lowercases hex)",
|
|
195
|
+
"inputs": {
|
|
196
|
+
"prompt_text": "hello world",
|
|
197
|
+
"tenant_id": "ABCDEF12-3456-7890-ABCD-EF1234567890"
|
|
198
|
+
},
|
|
199
|
+
"expected_output": "27ad8586fff06972454564d1fe1f447877a52807142815d2eb3e03f962152486"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "FXU1",
|
|
203
|
+
"fn": "derive_uuid_from_signature",
|
|
204
|
+
"description": "decision_id scope",
|
|
205
|
+
"inputs": {
|
|
206
|
+
"signature": "sig-abc",
|
|
207
|
+
"scope": "decision_id"
|
|
208
|
+
},
|
|
209
|
+
"expected_output": "5f870046-6d3e-4e1d-87bd-d3cbb46ec8e8"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "FXU2",
|
|
213
|
+
"fn": "derive_uuid_from_signature",
|
|
214
|
+
"description": "llm_call_id scope (same sig as FXU1)",
|
|
215
|
+
"inputs": {
|
|
216
|
+
"signature": "sig-abc",
|
|
217
|
+
"scope": "llm_call_id"
|
|
218
|
+
},
|
|
219
|
+
"expected_output": "7c0559d1-18d5-4a6b-9e3e-22a039284498"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "FXU3",
|
|
223
|
+
"fn": "derive_uuid_from_signature",
|
|
224
|
+
"description": "audit_chain scope",
|
|
225
|
+
"inputs": {
|
|
226
|
+
"signature": "audit-row-v1|tenant-a|2026-06-07",
|
|
227
|
+
"scope": "audit_chain"
|
|
228
|
+
},
|
|
229
|
+
"expected_output": "443453c9-c3d0-4638-a95e-f30abcf3e000"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "FXU4",
|
|
233
|
+
"fn": "derive_uuid_from_signature",
|
|
234
|
+
"description": "Custom scope string",
|
|
235
|
+
"inputs": {
|
|
236
|
+
"signature": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
237
|
+
"scope": "custom-test-scope"
|
|
238
|
+
},
|
|
239
|
+
"expected_output": "cf460197-b943-49a9-b936-dfb4b830c9b2"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "FXOA1",
|
|
243
|
+
"fn": "derive_agent_signature",
|
|
244
|
+
"description": "D08 openai_agents — simple string input, no system instructions",
|
|
245
|
+
"inputs": {
|
|
246
|
+
"input": "Say hi",
|
|
247
|
+
"system_instructions": null
|
|
248
|
+
},
|
|
249
|
+
"expected_output": "0b8e468f6a5faab3cf0459d24035a33c"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"id": "FXOA2",
|
|
253
|
+
"fn": "derive_agent_signature",
|
|
254
|
+
"description": "D08 openai_agents — string input + system instructions",
|
|
255
|
+
"inputs": {
|
|
256
|
+
"input": "Say hi",
|
|
257
|
+
"system_instructions": "Be polite"
|
|
258
|
+
},
|
|
259
|
+
"expected_output": "bb805e8a571a9ee6772e1b3985575deb"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "FXOA3",
|
|
263
|
+
"fn": "derive_agent_signature",
|
|
264
|
+
"description": "D08 openai_agents — short ASCII input + sys instructions (string-shape, cross-language byte-equal)",
|
|
265
|
+
"inputs": {
|
|
266
|
+
"input": "tools test",
|
|
267
|
+
"system_instructions": "Use the calculator tool"
|
|
268
|
+
},
|
|
269
|
+
"_note": "v0.1.x cross-language byte equality is limited to string-shape inputs because Python repr(list-of-dict) and TS JSON.stringify(list-of-dict) emit different byte sequences (single vs double quotes). The TS adapter falls back to JSON.stringify for non-string inputs, the Python adapter uses repr; the design.md §8 rendering quirk documents this divergence. A future v2.json could mint a canonical JSON form on both sides to close this gap; for now, FXOA* rows ship string-input only.",
|
|
270
|
+
"expected_output": "d2b0f7783bcbf3be27cc841b98e4c4ec"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "FXOA4",
|
|
274
|
+
"fn": "derive_agent_signature",
|
|
275
|
+
"description": "D08 openai_agents — empty string input + empty system",
|
|
276
|
+
"inputs": {
|
|
277
|
+
"input": "",
|
|
278
|
+
"system_instructions": ""
|
|
279
|
+
},
|
|
280
|
+
"expected_output": "7d6395228c93bac41591f4c6b65aea03"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"id": "FXOA5",
|
|
284
|
+
"fn": "derive_agent_signature",
|
|
285
|
+
"description": "D08 openai_agents — multi-byte UTF-8 input + CJK tenant",
|
|
286
|
+
"inputs": {
|
|
287
|
+
"input": "Hello, 世界!",
|
|
288
|
+
"system_instructions": null
|
|
289
|
+
},
|
|
290
|
+
"expected_output": "3941e46a71c2a2cf4b196f5bb6c21e87"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"id": "FXOA1_IK",
|
|
294
|
+
"fn": "derive_idempotency_key",
|
|
295
|
+
"description": "D08 openai_agents — FXOA1 signature → idempotencyKey for runId=r-1",
|
|
296
|
+
"inputs": {
|
|
297
|
+
"tenant_id": "t-1",
|
|
298
|
+
"session_id": "s-1",
|
|
299
|
+
"run_id": "r-1",
|
|
300
|
+
"step_id": "r-1:oai-call:0b8e468f6a5faab3",
|
|
301
|
+
"llm_call_id": "9fa6e431-89e0-403e-8eee-2b252a7bd77c",
|
|
302
|
+
"trigger": "LLM_CALL_PRE"
|
|
303
|
+
},
|
|
304
|
+
"expected_output": "sg-e4d1ba4a538f53e1f25839e227de8f1f"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"id": "FXOA1_DEC",
|
|
308
|
+
"fn": "derive_uuid_from_signature",
|
|
309
|
+
"description": "D08 openai_agents — FXOA1 signature → decision_id UUID",
|
|
310
|
+
"inputs": {
|
|
311
|
+
"signature": "0b8e468f6a5faab3cf0459d24035a33c",
|
|
312
|
+
"scope": "decision_id"
|
|
313
|
+
},
|
|
314
|
+
"expected_output": "b18ca2dd-a146-4d69-952f-c6215b6b4d75"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"id": "FXOA1_LLM",
|
|
318
|
+
"fn": "derive_uuid_from_signature",
|
|
319
|
+
"description": "D08 openai_agents — FXOA1 signature → llm_call_id UUID",
|
|
320
|
+
"inputs": {
|
|
321
|
+
"signature": "0b8e468f6a5faab3cf0459d24035a33c",
|
|
322
|
+
"scope": "llm_call_id"
|
|
323
|
+
},
|
|
324
|
+
"expected_output": "9fa6e431-89e0-403e-8eee-2b252a7bd77c"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|