@arnilo/prism 0.7.0 → 0.9.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 +73 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +15 -2
- package/dist/agent-approval.js +5 -1
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +91 -10
- package/dist/agent-run-state.d.ts +34 -2
- package/dist/agent-run-state.js +68 -6
- package/dist/agent-session/helpers.js +20 -1
- package/dist/agent-session/session/assemble.js +250 -27
- package/dist/agent-session/session/persist.d.ts +27 -0
- package/dist/agent-session/session/persist.js +94 -12
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +197 -25
- package/dist/agent-session/session/tool-round.js +24 -2
- package/dist/agent-session/session/types.d.ts +36 -2
- package/dist/agent-session/session.d.ts +40 -4
- package/dist/agent-session/session.js +78 -5
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/checkpoints.js +7 -11
- package/dist/context-budget.d.ts +2 -1
- package/dist/context-budget.js +24 -2
- package/dist/contracts-core/agent.d.ts +30 -0
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +15 -0
- package/dist/contracts-core/guardrail-packs.d.ts +41 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/provider.d.ts +25 -0
- package/dist/contracts-core/run-limits.d.ts +21 -0
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +90 -4
- package/dist/contracts-run-state.d.ts +82 -6
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/guardrail-packs/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +16 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +53 -0
- package/dist/guardrails.d.ts +20 -1
- package/dist/guardrails.js +268 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +19 -10
- package/dist/index.js +11 -6
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +11 -3
- package/dist/provider-events.js +62 -4
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +150 -0
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +46 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +30 -0
- package/dist/testing/prefix-stability-conformance.js +104 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +11 -3
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/ag-ui.md +5 -0
- package/docs/agent-events.md +68 -1
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +5 -3
- package/docs/attention-compiler.md +89 -8
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-observational-memory.md +34 -7
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +129 -0
- package/docs/embeddings.md +5 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +84 -1
- package/docs/guardrails.md +71 -2
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +66 -29
- package/docs/input-and-prompt-assembly.md +3 -3
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/middleware-hooks.md +38 -2
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +43 -0
- package/docs/model-registry.md +12 -2
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +36 -3
- package/docs/peer-dependencies.md +6 -6
- package/docs/policy-and-audit.md +13 -1
- package/docs/postgres-persistence.md +1 -1
- package/docs/prefix-stability-conformance.md +93 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/public-contracts.md +2 -2
- package/docs/rag.md +102 -4
- package/docs/release-and-install.md +55 -47
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +57 -6
- package/docs/scoped-agent-memory.md +262 -0
- package/docs/server.md +2 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +32 -12
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/tools.md +17 -0
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/docs/workflows.md +5 -0
- package/package.json +9 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsonObject } from "../contracts.js";
|
|
1
|
+
import type { JsonObject, ProviderFailureClass } from "../contracts.js";
|
|
2
2
|
export declare const DEFAULT_MAX_EVENT_BYTES = 262144;
|
|
3
3
|
export declare const DEFAULT_MAX_BUFFER_BYTES = 524288;
|
|
4
4
|
export declare const DEFAULT_MAX_RESPONSE_BODY_BYTES = 65536;
|
|
@@ -15,6 +15,8 @@ export interface SseEvent {
|
|
|
15
15
|
readonly comments?: readonly string[];
|
|
16
16
|
}
|
|
17
17
|
export type ProviderTransportErrorCode = "sse_buffer_overflow" | "sse_event_overflow" | "response_body_overflow" | "aborted" | "invalid_json_arguments" | "incomplete_delta" | "response_body_shape";
|
|
18
|
+
/** Maps already-captured provider transport evidence to advisory outcome metadata. */
|
|
19
|
+
export declare function classifyProviderFailure(error: unknown): ProviderFailureClass;
|
|
18
20
|
export declare class ProviderTransportError extends Error {
|
|
19
21
|
readonly code: ProviderTransportErrorCode;
|
|
20
22
|
readonly limitBytes?: number;
|
|
@@ -3,6 +3,22 @@ export const DEFAULT_MAX_EVENT_BYTES = 262_144;
|
|
|
3
3
|
export const DEFAULT_MAX_BUFFER_BYTES = 524_288;
|
|
4
4
|
export const DEFAULT_MAX_RESPONSE_BODY_BYTES = 65_536;
|
|
5
5
|
export const DEFAULT_MAX_ARGUMENT_BYTES = 262_144;
|
|
6
|
+
const QUOTA_BODY = /quota|usage[\s_-]*limit|insufficient[\s_-]*(?:quota|credit|balance)|billing[\s_-]*(?:limit|quota)/i;
|
|
7
|
+
const TRANSIENT_NETWORK_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ENETUNREACH", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
8
|
+
/** Maps already-captured provider transport evidence to advisory outcome metadata. */
|
|
9
|
+
export function classifyProviderFailure(error) {
|
|
10
|
+
const status = readHttpStatus(error);
|
|
11
|
+
if (status === 401 || status === 403)
|
|
12
|
+
return "auth";
|
|
13
|
+
if (status === 429)
|
|
14
|
+
return QUOTA_BODY.test(errorMessage(error)) ? "quota" : "rate_limited";
|
|
15
|
+
if (status !== undefined)
|
|
16
|
+
return status >= 500 ? "transient" : status >= 400 ? "permanent" : "unknown";
|
|
17
|
+
const cause = readField(error, "cause");
|
|
18
|
+
if (isTransientNetworkCode(readField(error, "code")) || isTransientNetworkCode(readField(cause, "code")))
|
|
19
|
+
return "transient";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
6
22
|
export class ProviderTransportError extends Error {
|
|
7
23
|
code;
|
|
8
24
|
limitBytes;
|
|
@@ -22,8 +38,28 @@ export function httpStatusError(prefix, response, bodyText) {
|
|
|
22
38
|
const hint = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
23
39
|
if (hint !== undefined)
|
|
24
40
|
error.retryAfterMs = hint;
|
|
41
|
+
Object.defineProperty(error, "failureClass", { value: classifyProviderFailure(error), enumerable: true });
|
|
25
42
|
return error;
|
|
26
43
|
}
|
|
44
|
+
function readHttpStatus(error) {
|
|
45
|
+
for (const key of ["code", "status", "statusCode"]) {
|
|
46
|
+
const value = readField(error, key);
|
|
47
|
+
const status = typeof value === "number" ? value : typeof value === "string" && /^\d{3}$/.test(value) ? Number(value) : undefined;
|
|
48
|
+
if (status !== undefined && status >= 100 && status <= 599)
|
|
49
|
+
return status;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function readField(error, key) {
|
|
54
|
+
return error && typeof error === "object" && key in error ? error[key] : undefined;
|
|
55
|
+
}
|
|
56
|
+
function isTransientNetworkCode(value) {
|
|
57
|
+
return typeof value === "string" && TRANSIENT_NETWORK_CODES.has(value.toUpperCase());
|
|
58
|
+
}
|
|
59
|
+
function errorMessage(error) {
|
|
60
|
+
const cause = readField(error, "cause");
|
|
61
|
+
return [readField(error, "message"), readField(cause, "message")].filter((value) => typeof value === "string").join(" ");
|
|
62
|
+
}
|
|
27
63
|
/** Parse a `Retry-After` header (delay-seconds or HTTP-date) into milliseconds. */
|
|
28
64
|
export function parseRetryAfterMs(value, now = Date.now()) {
|
|
29
65
|
if (!value)
|
package/dist/redaction.js
CHANGED
|
@@ -167,19 +167,22 @@ export function errorToErrorInfo(error, secrets = []) {
|
|
|
167
167
|
const code = readErrorCode(error);
|
|
168
168
|
const retry = readRetryAfterMs(error);
|
|
169
169
|
const retryAfter = retry !== undefined ? { retryAfterMs: retry } : {};
|
|
170
|
+
const failureClass = readProviderFailureClass(error);
|
|
171
|
+
const failure = failureClass === undefined ? {} : { failureClass };
|
|
170
172
|
if (error instanceof Error) {
|
|
171
173
|
return {
|
|
172
174
|
name: error.name,
|
|
173
175
|
message: redactSecrets(error.message, secrets),
|
|
174
176
|
code,
|
|
177
|
+
...failure,
|
|
175
178
|
...retryAfter,
|
|
176
179
|
cause: error.cause ? redactSecrets(String(error.cause), secrets) : undefined,
|
|
177
180
|
};
|
|
178
181
|
}
|
|
179
182
|
if (error && typeof error === "object" && "message" in error) {
|
|
180
|
-
return { message: redactSecrets(String(error.message), secrets), code, ...retryAfter };
|
|
183
|
+
return { message: redactSecrets(String(error.message), secrets), code, ...failure, ...retryAfter };
|
|
181
184
|
}
|
|
182
|
-
return { message: redactSecrets(String(error), secrets), code, ...retryAfter };
|
|
185
|
+
return { message: redactSecrets(String(error), secrets), code, ...failure, ...retryAfter };
|
|
183
186
|
}
|
|
184
187
|
function readErrorCode(error) {
|
|
185
188
|
if (!error || typeof error !== "object" || !("code" in error))
|
|
@@ -193,4 +196,17 @@ function readRetryAfterMs(error) {
|
|
|
193
196
|
const value = error.retryAfterMs;
|
|
194
197
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
195
198
|
}
|
|
199
|
+
function readProviderFailureClass(error) {
|
|
200
|
+
if (!error || typeof error !== "object" || !("failureClass" in error))
|
|
201
|
+
return undefined;
|
|
202
|
+
const value = error.failureClass;
|
|
203
|
+
return value === "quota" ||
|
|
204
|
+
value === "auth" ||
|
|
205
|
+
value === "rate_limited" ||
|
|
206
|
+
value === "transient" ||
|
|
207
|
+
value === "permanent" ||
|
|
208
|
+
value === "unknown"
|
|
209
|
+
? value
|
|
210
|
+
: undefined;
|
|
211
|
+
}
|
|
196
212
|
//# sourceMappingURL=redaction.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Agent, AgentSessionConfig, GuardrailStage, RunOptions } from "./contracts.js";
|
|
2
|
+
/** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
|
|
3
|
+
export declare const RUN_BUNDLE_SCHEMA_VERSION = 1;
|
|
4
|
+
/** Inspectable projection of the inputs a run actually resolves to. Frozen JSON, safe to persist and diff. */
|
|
5
|
+
export interface RunBundleSnapshot {
|
|
6
|
+
readonly schemaVersion: number;
|
|
7
|
+
/** `sha256:<64 hex>` over the canonicalized, redacted snapshot (this field excluded). */
|
|
8
|
+
readonly digest: string;
|
|
9
|
+
/** `agentFingerprint()` of the same agent/revision: the durable-resume identity behind this snapshot. */
|
|
10
|
+
readonly fingerprint: string;
|
|
11
|
+
readonly agent: {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly definitionRevision: string | null;
|
|
14
|
+
};
|
|
15
|
+
readonly systemPrompt: {
|
|
16
|
+
readonly disabled: boolean;
|
|
17
|
+
/** Digest of `AgentConfig.instructions`; the body never leaves the process. */
|
|
18
|
+
readonly instructionsDigest: string | null;
|
|
19
|
+
readonly contributions: readonly {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly mode: string | null;
|
|
22
|
+
readonly source: string | null;
|
|
23
|
+
readonly digest: string;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
readonly skills: readonly {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly instructionsDigest: string | null;
|
|
29
|
+
readonly toolNames: readonly string[];
|
|
30
|
+
}[];
|
|
31
|
+
/** Effective tool set: `run.toolNames` narrowing already applied, schemas reduced to digests. */
|
|
32
|
+
readonly tools: readonly {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly schemaDigest: string;
|
|
35
|
+
readonly exclusive: boolean;
|
|
36
|
+
readonly effect: string | null;
|
|
37
|
+
}[];
|
|
38
|
+
readonly activeSkills: readonly string[] | null;
|
|
39
|
+
readonly guardrails: readonly {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly stage: GuardrailStage;
|
|
42
|
+
readonly revision: string | null;
|
|
43
|
+
}[];
|
|
44
|
+
readonly loop: {
|
|
45
|
+
readonly strategy: string;
|
|
46
|
+
readonly revision: string | null;
|
|
47
|
+
};
|
|
48
|
+
readonly thinkingLevel: string | null;
|
|
49
|
+
readonly limits: Readonly<import("./contracts.js").ResolvedRunLimits>;
|
|
50
|
+
/** Host-shaped JSON as configured (`true`/`false`/options); `null` when unset. */
|
|
51
|
+
readonly attentionCompiler: unknown;
|
|
52
|
+
readonly model: {
|
|
53
|
+
readonly provider: string | null;
|
|
54
|
+
readonly model: string | null;
|
|
55
|
+
};
|
|
56
|
+
readonly requestPolicies: readonly string[];
|
|
57
|
+
/** Kinds only — never a connection string, path, or credential. */
|
|
58
|
+
readonly storage: {
|
|
59
|
+
readonly sessionStore: {
|
|
60
|
+
readonly kind: string;
|
|
61
|
+
readonly durable: boolean;
|
|
62
|
+
};
|
|
63
|
+
readonly checkpoints: {
|
|
64
|
+
readonly kind: string;
|
|
65
|
+
readonly durable: boolean;
|
|
66
|
+
};
|
|
67
|
+
readonly effectStore: {
|
|
68
|
+
readonly kind: string;
|
|
69
|
+
readonly durable: boolean;
|
|
70
|
+
};
|
|
71
|
+
readonly memory: {
|
|
72
|
+
readonly kind: string;
|
|
73
|
+
readonly durable: boolean;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export interface RunBundleSnapshotInput {
|
|
78
|
+
readonly agent: Agent;
|
|
79
|
+
/** Session-level inputs (store, leaf, cache TTL) that change what the run reads and writes. */
|
|
80
|
+
readonly config?: AgentSessionConfig;
|
|
81
|
+
readonly run?: RunOptions;
|
|
82
|
+
/** Optional memory store instance; only its kind/durability label is read, never its contents. */
|
|
83
|
+
readonly memory?: unknown;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
|
|
87
|
+
* The counterpart of `agentFingerprint` for humans — same inputs, named fields, one stable digest to pin.
|
|
88
|
+
*/
|
|
89
|
+
export declare function snapshotRunBundle(input: RunBundleSnapshotInput): RunBundleSnapshot;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { agentFingerprint, BUILT_IN_LOOP_REVISIONS } from "./agent-run-state.js";
|
|
3
|
+
import { describeGuardrailPacks } from "./guardrails.js";
|
|
4
|
+
import { describeStorage } from "./host-composition.js";
|
|
5
|
+
import { canonicalizeJsonSchema } from "./providers/schema.js";
|
|
6
|
+
import { resolveRunLimits } from "./run-limits.js";
|
|
7
|
+
import { selectRunTools } from "./tools.js";
|
|
8
|
+
/** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
|
|
9
|
+
export const RUN_BUNDLE_SCHEMA_VERSION = 1;
|
|
10
|
+
/** Bounded by construction; a bundle larger than this is a host bug, not a snapshot to retain. */
|
|
11
|
+
const MAX_RUN_BUNDLE_BYTES = 512 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
|
|
14
|
+
* The counterpart of `agentFingerprint` for humans — same inputs, named fields, one stable digest to pin.
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotRunBundle(input) {
|
|
17
|
+
const config = input.agent.config;
|
|
18
|
+
const run = input.run;
|
|
19
|
+
const redactor = run?.redactor ?? config.redactor;
|
|
20
|
+
const definitionRevision = run?.runState?.definitionRevision ?? config.runState?.definitionRevision ?? null;
|
|
21
|
+
const tools = selectRunTools(listTools(config.tools), run?.toolNames).tools;
|
|
22
|
+
const skills = listSkills(config.skills, run?.skills);
|
|
23
|
+
const model = run?.model ?? config.model;
|
|
24
|
+
const effectiveLoop = run?.loop ?? config.loop;
|
|
25
|
+
const systemPrompt = run?.systemPrompt ?? config.systemPrompt;
|
|
26
|
+
const policies = run?.providerRequestPolicies ?? config.providerRequestPolicies;
|
|
27
|
+
const snapshot = {
|
|
28
|
+
schemaVersion: RUN_BUNDLE_SCHEMA_VERSION,
|
|
29
|
+
fingerprint: agentFingerprint(input.agent, definitionRevision ?? ""),
|
|
30
|
+
agent: { id: agentId(input.agent), definitionRevision },
|
|
31
|
+
systemPrompt: {
|
|
32
|
+
disabled: systemPrompt === false,
|
|
33
|
+
instructionsDigest: hashText(config.instructions),
|
|
34
|
+
contributions: systemPrompt === false || systemPrompt === undefined
|
|
35
|
+
? []
|
|
36
|
+
: (Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt]).map((contribution) => ({
|
|
37
|
+
id: contribution.id,
|
|
38
|
+
mode: contribution.mode ?? null,
|
|
39
|
+
source: contribution.source ?? null,
|
|
40
|
+
digest: hashText(contribution.text) ?? "",
|
|
41
|
+
})),
|
|
42
|
+
},
|
|
43
|
+
skills: skills.map((skill) => ({
|
|
44
|
+
name: skill.name,
|
|
45
|
+
instructionsDigest: hashText(skill.instructions),
|
|
46
|
+
toolNames: skill.toolNames ?? [],
|
|
47
|
+
})),
|
|
48
|
+
tools: tools.map((tool) => ({
|
|
49
|
+
name: tool.name,
|
|
50
|
+
schemaDigest: hashJson(canonicalizeJsonSchema(tool.parameters ?? { type: "object" })),
|
|
51
|
+
exclusive: tool.exclusive === true,
|
|
52
|
+
effect: tool.effect === undefined ? null : typeof tool.effect === "function" ? "classifier" : tool.effect.kind,
|
|
53
|
+
})),
|
|
54
|
+
activeSkills: run?.activeSkills ?? null,
|
|
55
|
+
guardrails: [...guardrailRows(config.guardrails, run?.guardrails), ...describeGuardrailPacks(input.config?.guardrailPacks)],
|
|
56
|
+
loop: loopIdentity(effectiveLoop),
|
|
57
|
+
thinkingLevel: run?.thinkingLevel ?? config.thinkingLevel ?? null,
|
|
58
|
+
limits: resolveRunLimits(config.limits, run?.limits),
|
|
59
|
+
attentionCompiler: run?.attentionCompiler ?? config.attentionCompiler ?? null,
|
|
60
|
+
model: {
|
|
61
|
+
provider: typeof model === "string" ? (config.provider?.id ?? null) : (model?.provider ?? null),
|
|
62
|
+
model: typeof model === "string" ? model : (model?.model ?? null),
|
|
63
|
+
},
|
|
64
|
+
requestPolicies: policies === undefined ? [] : (Array.isArray(policies) ? policies : [policies]).map((policy) => policy.name),
|
|
65
|
+
storage: {
|
|
66
|
+
sessionStore: kindOf(describeStorage(input.config?.store ?? config.store, undefined)),
|
|
67
|
+
checkpoints: kindOf(describeStorage(undefined, (run?.runState ?? config.runState)?.checkpoints)),
|
|
68
|
+
effectStore: kindOf(describeStorage(run?.effectStore ?? config.effectStore, undefined)),
|
|
69
|
+
memory: kindOf(describeStorage(input.memory, undefined)),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const redacted = redactStrings(snapshot, redactor);
|
|
73
|
+
const bundle = deepFreeze({ ...redacted, digest: hashJson(canonicalizeJsonSchema(redacted)) });
|
|
74
|
+
const bytes = Buffer.byteLength(JSON.stringify(bundle), "utf8");
|
|
75
|
+
if (bytes > MAX_RUN_BUNDLE_BYTES) {
|
|
76
|
+
throw new TypeError(`Run bundle snapshot exceeds ${MAX_RUN_BUNDLE_BYTES} bytes`);
|
|
77
|
+
}
|
|
78
|
+
return bundle;
|
|
79
|
+
}
|
|
80
|
+
/** Keeps a store label a label: a declared `kind` that is really a connection string or path becomes `custom`. */
|
|
81
|
+
function kindOf(described) {
|
|
82
|
+
const raw = described.kind.toLowerCase();
|
|
83
|
+
return { kind: /^[a-z0-9_.-]{1,64}$/.test(raw) ? raw : "custom", durable: described.durable };
|
|
84
|
+
}
|
|
85
|
+
function agentId(agent) {
|
|
86
|
+
return agent.config.id ?? agent.config.name ?? "agent";
|
|
87
|
+
}
|
|
88
|
+
function listTools(tools) {
|
|
89
|
+
if (!tools)
|
|
90
|
+
return [];
|
|
91
|
+
return "list" in tools ? tools.list() : tools;
|
|
92
|
+
}
|
|
93
|
+
function listSkills(skills, runSkills) {
|
|
94
|
+
const listed = !skills ? [] : "list" in skills ? skills.list() : skills;
|
|
95
|
+
const byName = new Map(listed.map((skill) => [skill.name, skill]));
|
|
96
|
+
for (const skill of runSkills ?? [])
|
|
97
|
+
byName.set(skill.name, skill);
|
|
98
|
+
return [...byName.values()];
|
|
99
|
+
}
|
|
100
|
+
function guardrailRows(configGuardrails, runGuardrails) {
|
|
101
|
+
const rows = [];
|
|
102
|
+
for (const guardrails of [configGuardrails, runGuardrails]) {
|
|
103
|
+
if (!guardrails)
|
|
104
|
+
continue;
|
|
105
|
+
for (const stage of ["input", "output", "tool_input", "tool_output"]) {
|
|
106
|
+
const key = stage === "tool_input" ? "toolInput" : stage === "tool_output" ? "toolOutput" : stage;
|
|
107
|
+
for (const guardrail of guardrails[key] ?? []) {
|
|
108
|
+
rows.push({ name: guardrail.name, stage, revision: guardrail.revision ?? null });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
function loopIdentity(loop) {
|
|
115
|
+
if (typeof loop === "object" && loop && "strategy" in loop) {
|
|
116
|
+
return { strategy: loop.strategy, revision: BUILT_IN_LOOP_REVISIONS[loop.strategy] ?? null };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
strategy: loop?.name ?? "single-shot",
|
|
120
|
+
revision: loop?.revision ?? BUILT_IN_LOOP_REVISIONS["single-shot"] ?? null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** `sha256:<64 hex>` over UTF-8 text — the same convention as `hashPromptBody` in `@arnilo/prism-core`. */
|
|
124
|
+
function hashText(text) {
|
|
125
|
+
if (text === undefined)
|
|
126
|
+
return null;
|
|
127
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
128
|
+
}
|
|
129
|
+
function hashJson(value) {
|
|
130
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex")}`;
|
|
131
|
+
}
|
|
132
|
+
/** Redacts every string field so a pinned snapshot can never carry a secret. */
|
|
133
|
+
function redactStrings(value, redactor) {
|
|
134
|
+
if (typeof value === "string")
|
|
135
|
+
return redactor ? redactor.redact(value) : value;
|
|
136
|
+
if (!value || typeof value !== "object")
|
|
137
|
+
return value;
|
|
138
|
+
if (Array.isArray(value))
|
|
139
|
+
return value.map((item) => redactStrings(item, redactor));
|
|
140
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactStrings(item, redactor)]));
|
|
141
|
+
}
|
|
142
|
+
function deepFreeze(value) {
|
|
143
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
144
|
+
Object.freeze(value);
|
|
145
|
+
for (const item of Object.values(value))
|
|
146
|
+
deepFreeze(item);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=run-bundle.js.map
|
package/dist/run-limits.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedRunLimits, RunLimitBreach, RunLimitCounters, RunLimitName, RunLimits, Usage } from "./contracts.js";
|
|
1
|
+
import type { BudgetAxisUsage, BudgetConsumedCounters, ResolvedRunLimits, RunLimitBreach, RunLimitCounters, RunLimitName, RunLimits, ToolCallSummary, Usage } from "./contracts.js";
|
|
2
2
|
export declare const DEFAULT_RUN_LIMITS: Readonly<{
|
|
3
3
|
maxTurns: 16;
|
|
4
4
|
maxProviderAttempts: 24;
|
|
@@ -51,3 +51,13 @@ export declare class RunLimitTracker {
|
|
|
51
51
|
private exceed;
|
|
52
52
|
}
|
|
53
53
|
export declare function createRunLimitTracker(limits: RunLimits | undefined, options?: RunLimitTrackerOptions): RunLimitTracker;
|
|
54
|
+
/**
|
|
55
|
+
* Build the `budget_exhausted` payload (plan 087 T2): which axis fired, the counters a host reads
|
|
56
|
+
* first, the closest other axes, and the last dispatched tool calls (hashes only).
|
|
57
|
+
*/
|
|
58
|
+
export declare function describeBudgetExhaustion(tracker: RunLimitTracker, breach: RunLimitBreach, recentToolCalls: readonly ToolCallSummary[]): {
|
|
59
|
+
limit: RunLimitName;
|
|
60
|
+
consumed: BudgetConsumedCounters;
|
|
61
|
+
closestOtherAxes: BudgetAxisUsage[];
|
|
62
|
+
recentToolCalls: ToolCallSummary[];
|
|
63
|
+
};
|
package/dist/run-limits.js
CHANGED
|
@@ -263,4 +263,50 @@ export class RunLimitTracker {
|
|
|
263
263
|
export function createRunLimitTracker(limits, options) {
|
|
264
264
|
return new RunLimitTracker(resolveRunLimits(undefined, limits), options);
|
|
265
265
|
}
|
|
266
|
+
/** Product axes reported as "how close was everything else" (plan 087 T2). Request/response bytes
|
|
267
|
+
* stay out: their caps are per-frame, so a run-lifetime ratio would be meaningless. */
|
|
268
|
+
const ATTRIBUTION_AXES = [
|
|
269
|
+
{ axis: "maxTurns", counter: "turns" },
|
|
270
|
+
{ axis: "maxProviderAttempts", counter: "providerAttempts" },
|
|
271
|
+
{ axis: "maxToolRounds", counter: "toolRounds" },
|
|
272
|
+
{ axis: "maxToolCalls", counter: "toolCalls" },
|
|
273
|
+
{ axis: "maxWallTimeMs", counter: "wallTimeMs" },
|
|
274
|
+
{ axis: "maxInputTokens", counter: "inputTokens" },
|
|
275
|
+
{ axis: "maxOutputTokens", counter: "outputTokens" },
|
|
276
|
+
{ axis: "maxTotalTokens", counter: "totalTokens" },
|
|
277
|
+
{ axis: "maxCost", counter: "cost" },
|
|
278
|
+
];
|
|
279
|
+
const CLOSEST_AXIS_COUNT = 3;
|
|
280
|
+
function axisCap(limits, axis) {
|
|
281
|
+
if (axis === "maxCost")
|
|
282
|
+
return limits.maxCost?.amount;
|
|
283
|
+
const value = limits[axis];
|
|
284
|
+
return typeof value === "number" ? value : null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Build the `budget_exhausted` payload (plan 087 T2): which axis fired, the counters a host reads
|
|
288
|
+
* first, the closest other axes, and the last dispatched tool calls (hashes only).
|
|
289
|
+
*/
|
|
290
|
+
export function describeBudgetExhaustion(tracker, breach, recentToolCalls) {
|
|
291
|
+
const counters = tracker.snapshot();
|
|
292
|
+
const closestOtherAxes = ATTRIBUTION_AXES.filter(({ axis }) => axis !== breach.limit)
|
|
293
|
+
.flatMap(({ axis, counter }) => {
|
|
294
|
+
const cap = axisCap(tracker.limits, axis);
|
|
295
|
+
return typeof cap === "number" && cap > 0 ? [{ axis, cap, used: counters[counter] }] : [];
|
|
296
|
+
})
|
|
297
|
+
.map(({ axis, cap, used }) => ({ axis, usedRatio: Math.round(Math.min(1, used / cap) * 10_000) / 10_000 }))
|
|
298
|
+
.sort((a, b) => b.usedRatio - a.usedRatio)
|
|
299
|
+
.slice(0, CLOSEST_AXIS_COUNT);
|
|
300
|
+
return {
|
|
301
|
+
limit: breach.limit,
|
|
302
|
+
consumed: {
|
|
303
|
+
turns: counters.turns,
|
|
304
|
+
inputTokens: counters.inputTokens,
|
|
305
|
+
providerAttempts: counters.providerAttempts,
|
|
306
|
+
requestBytes: counters.requestBytes,
|
|
307
|
+
},
|
|
308
|
+
closestOtherAxes,
|
|
309
|
+
recentToolCalls: [...recentToolCalls],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
266
312
|
//# sourceMappingURL=run-limits.js.map
|
package/dist/session-stores.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BranchReader, type Message, type SessionBranchRead, type SessionEntry, type SessionStore } from "./contracts.js";
|
|
1
|
+
import { type BranchReader, type Message, type PersistencePage, type SessionBranchRead, type SessionEntry, type SessionSearchHit, type SessionSearchQuery, type SessionStore } from "./contracts.js";
|
|
2
2
|
export interface CreateSessionEntryOptions extends Omit<SessionEntry, "id" | "timestamp"> {
|
|
3
3
|
readonly id?: string;
|
|
4
4
|
readonly timestamp?: string;
|
|
@@ -40,4 +40,15 @@ export interface CreateMemorySessionStoreOptions {
|
|
|
40
40
|
readonly maxLinearBytes?: number;
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
/** Resolved bounds for the capped linear session scan. */
|
|
44
|
+
export interface LinearSearchCaps {
|
|
45
|
+
readonly sessions: number;
|
|
46
|
+
readonly entries: number;
|
|
47
|
+
readonly bytes: number;
|
|
48
|
+
}
|
|
43
49
|
export declare function createMemorySessionStore(initialEntries?: readonly SessionEntry[], options?: CreateMemorySessionStoreOptions): SessionStore;
|
|
50
|
+
/**
|
|
51
|
+
* Shared linear (unindexed) session search over already-grouped entries: the memory store's default
|
|
52
|
+
* mode and the JSONL store's implementation. `caps` defaults to the contract linear caps.
|
|
53
|
+
*/
|
|
54
|
+
export declare function searchLinearSessions(bySession: Map<string, SessionEntry[]>, leafBySession: Map<string, string>, query: SessionSearchQuery, caps?: LinearSearchCaps): PersistencePage<SessionSearchHit>;
|
package/dist/session-stores.js
CHANGED
|
@@ -133,7 +133,7 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
|
|
|
133
133
|
async searchSessions(query) {
|
|
134
134
|
if (mode === "unsupported")
|
|
135
135
|
throw new SessionSearchUnsupportedError();
|
|
136
|
-
return
|
|
136
|
+
return searchLinearSessions(bySession, leafBySession, query, searchCaps);
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
function add(entry, options) {
|
|
@@ -175,7 +175,11 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
|
|
|
175
175
|
leafBySession.set(entry.sessionId, entry.id);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Shared linear (unindexed) session search over already-grouped entries: the memory store's default
|
|
180
|
+
* mode and the JSONL store's implementation. `caps` defaults to the contract linear caps.
|
|
181
|
+
*/
|
|
182
|
+
export function searchLinearSessions(bySession, leafBySession, query, caps = resolveLinearSearchCaps(undefined)) {
|
|
179
183
|
const q = resolveSessionSearchQuery(query);
|
|
180
184
|
q.signal?.throwIfAborted();
|
|
181
185
|
let sessionsScanned = 0;
|
|
@@ -203,13 +207,20 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
|
|
|
203
207
|
let matchedQuery = false;
|
|
204
208
|
let matchedProvider = false;
|
|
205
209
|
let matchedModel = false;
|
|
210
|
+
let matchedKind = false;
|
|
211
|
+
let matchedEntry;
|
|
206
212
|
let snippetSource;
|
|
213
|
+
let turn = 0;
|
|
207
214
|
for (const entry of entries) {
|
|
208
215
|
if (entriesScanned >= caps.entries)
|
|
209
216
|
break;
|
|
210
217
|
if (bytesScanned >= caps.bytes)
|
|
211
218
|
break;
|
|
212
219
|
entriesScanned += 1;
|
|
220
|
+
turn += 1;
|
|
221
|
+
const kindMatches = q.kind === undefined || q.kind.includes(entry.kind);
|
|
222
|
+
if (kindMatches)
|
|
223
|
+
matchedKind = true;
|
|
213
224
|
const text = entrySearchText(entry);
|
|
214
225
|
bytesScanned += utf8Bytes(text) + utf8Bytes(entry.label) + utf8Bytes(entry.summary);
|
|
215
226
|
if (entry.timestamp > updatedAt)
|
|
@@ -234,11 +245,12 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
|
|
|
234
245
|
matchedLabel = true;
|
|
235
246
|
if (q.summary && entry.summary?.includes(q.summary))
|
|
236
247
|
matchedSummary = true;
|
|
237
|
-
if (q.query) {
|
|
248
|
+
if (q.query && kindMatches) {
|
|
238
249
|
const hay = `${entry.label ?? ""}\n${entry.summary ?? ""}\n${text}`;
|
|
239
250
|
if (hay.includes(q.query)) {
|
|
240
251
|
matchedQuery = true;
|
|
241
|
-
|
|
252
|
+
matchedEntry ??= { entry, turn };
|
|
253
|
+
snippetSource ??= [entry.label, entry.summary, text].find((part) => part?.includes(q.query)) ?? hay;
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
if (q.provider && (entry.model?.provider === q.provider || metaProvider(entry) === q.provider))
|
|
@@ -260,6 +272,8 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
|
|
|
260
272
|
continue;
|
|
261
273
|
if (q.query && !matchedQuery)
|
|
262
274
|
continue;
|
|
275
|
+
if (q.kind && (q.query ? matchedEntry === undefined : !matchedKind))
|
|
276
|
+
continue;
|
|
263
277
|
if (q.provider && !matchedProvider)
|
|
264
278
|
continue;
|
|
265
279
|
if (q.model && !matchedModel)
|
|
@@ -271,6 +285,9 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
|
|
|
271
285
|
matches.push({
|
|
272
286
|
sessionId,
|
|
273
287
|
leafId: leafBySession.get(sessionId),
|
|
288
|
+
entryId: matchedEntry?.entry.id,
|
|
289
|
+
runId: matchedEntry?.entry.runId,
|
|
290
|
+
turn: matchedEntry?.turn,
|
|
274
291
|
updatedAt: updatedAt || undefined,
|
|
275
292
|
label,
|
|
276
293
|
summary,
|
|
@@ -28,14 +28,53 @@ export async function assertAgentEventSourceConforms(factory) {
|
|
|
28
28
|
const final = await source.page({ ...input, after: secondPage.items[0].cursor, limit: 10 });
|
|
29
29
|
equal(final.items.at(-1)?.record.id, terminal.id, "terminal page must include its terminal event");
|
|
30
30
|
equal(final.terminal, true, "terminal event must close only after prior events are delivered");
|
|
31
|
+
// A limit death delivers its attribution before the run's outcome: the breach and budget records
|
|
32
|
+
// are not terminal, the `error` that follows them is.
|
|
33
|
+
const breached = { ...input, runId: "run-limit" };
|
|
34
|
+
await source.append(event("event-limit-before", "turn_started", breached, "2026-01-01T00:00:04.000Z"));
|
|
35
|
+
const beforeLimitItem = (await source.page({ ...breached, limit: 1 })).items[0];
|
|
36
|
+
if (!beforeLimitItem)
|
|
37
|
+
throw new Error("limit-death precondition missing");
|
|
38
|
+
const beforeLimit = beforeLimitItem.cursor;
|
|
39
|
+
await source.append(event("event-limit", "run_limit_exceeded", breached, "2026-01-01T00:00:05.000Z"));
|
|
40
|
+
await source.append(event("event-attr", "budget_exhausted", breached, "2026-01-01T00:00:06.000Z"));
|
|
41
|
+
const breachPage = await source.page({ ...breached, after: beforeLimit, limit: 10 });
|
|
42
|
+
equal(breachPage.items.length, 2, "the breach and its attribution must page together");
|
|
43
|
+
equal(breachPage.terminal, false, "a limit breach must not close the stream; the run's error follows");
|
|
44
|
+
await source.append(event("event-limit-error", "error", breached, "2026-01-01T00:00:07.000Z"));
|
|
45
|
+
const deadPage = await source.page({ ...breached, after: beforeLimit, limit: 10 });
|
|
46
|
+
equal(deadPage.terminal, true, "the error after a limit breach closes the stream");
|
|
47
|
+
const death = source.subscribe(breached)[Symbol.asyncIterator]();
|
|
48
|
+
const delivered = [];
|
|
49
|
+
for (;;) {
|
|
50
|
+
const next = await death.next();
|
|
51
|
+
if (next.done)
|
|
52
|
+
break;
|
|
53
|
+
delivered.push(next.value.record.type);
|
|
54
|
+
}
|
|
55
|
+
equal(delivered.join(","), "turn_started,run_limit_exceeded,budget_exhausted,error", "a subscriber must read the breach and its attribution before the stream ends");
|
|
31
56
|
await rejects(() => source.page({ ...input, ownership: { ...ownership, tenantId: "tenant-b" }, after: page.nextCursor }), "foreign cursor must fail closed");
|
|
32
57
|
await rejects(() => source.append({ ...event("event-unredacted", "turn_started", input), redacted: false }), "unredacted append must fail");
|
|
33
58
|
await rejects(() => source.page({ ...input, limit: 0 }), "invalid page limit must fail");
|
|
34
59
|
}
|
|
35
60
|
function event(id, type, input, timestamp = "2026-01-01T00:00:00.000Z") {
|
|
61
|
+
const scoped = { sessionId: input.sessionId, runId: input.runId };
|
|
36
62
|
const event = type === "turn_started"
|
|
37
|
-
? { type,
|
|
38
|
-
:
|
|
63
|
+
? { type, ...scoped, turn: 1 }
|
|
64
|
+
: type === "run_limit_exceeded"
|
|
65
|
+
? { type, ...scoped, breach: { limit: "maxTurns", maximum: 1, observed: 2 } }
|
|
66
|
+
: type === "budget_exhausted"
|
|
67
|
+
? {
|
|
68
|
+
type,
|
|
69
|
+
...scoped,
|
|
70
|
+
limit: "maxTurns",
|
|
71
|
+
consumed: { turns: 2, inputTokens: 0, providerAttempts: 1, requestBytes: 0 },
|
|
72
|
+
closestOtherAxes: [],
|
|
73
|
+
recentToolCalls: [],
|
|
74
|
+
}
|
|
75
|
+
: type === "error"
|
|
76
|
+
? { type, ...scoped, error: { message: "run limit exceeded" } }
|
|
77
|
+
: { type, ...scoped };
|
|
39
78
|
return { id, ...input.ownership, sessionId: input.sessionId, runId: input.runId, type, timestamp, event, redacted: true };
|
|
40
79
|
}
|
|
41
80
|
function equal(actual, expected, message) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentConfig, Skill } from "../contracts.js";
|
|
2
|
+
export interface PrefixStabilityConformanceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The host's own agent config, minus `provider`, `providerSource`, and `skills`:
|
|
5
|
+
* the runner installs its fixture provider and fixture skill registry so the
|
|
6
|
+
* scenario is deterministic and comparable across hosts.
|
|
7
|
+
*/
|
|
8
|
+
readonly host: Omit<AgentConfig, "provider" | "providerSource" | "skills">;
|
|
9
|
+
/** Two distinct skills: the fixture loads `[0]` on the first turn and `[1]` on the second. */
|
|
10
|
+
readonly skills: readonly [Skill, Skill];
|
|
11
|
+
/** Minimum shared byte-prefix fraction between consecutive requests. Default `0.95`. */
|
|
12
|
+
readonly minContinuity?: number;
|
|
13
|
+
/** Turn inputs; defaults are fixed strings so runs are comparable across hosts. */
|
|
14
|
+
readonly inputs?: readonly [string, string];
|
|
15
|
+
}
|
|
16
|
+
export interface PrefixStabilityConformanceResult {
|
|
17
|
+
/** Provider requests captured by the fixture (two per turn: skill load, then completion). */
|
|
18
|
+
readonly requests: number;
|
|
19
|
+
/** Lowest shared-prefix fraction observed across consecutive captured requests. */
|
|
20
|
+
readonly minContinuity: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Drive a real session through two staggered skill loads and assert that each
|
|
24
|
+
* provider request keeps a byte-identical leading prefix (messages **and** tool
|
|
25
|
+
* schemas) with its predecessor. Progressive disclosure appends a loaded body
|
|
26
|
+
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
27
|
+
* rewrites the context block, the skill catalog, or any leading message per
|
|
28
|
+
* request fails with the offending request pair and the measured fraction.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runPrefixStabilityConformance(options: PrefixStabilityConformanceOptions): Promise<PrefixStabilityConformanceResult>;
|