@arnilo/prism 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +32 -20
- package/dist/agent-loops.d.ts +8 -1
- package/dist/agent-loops.js +57 -11
- package/dist/agents.js +70 -17
- package/dist/checkpoints.d.ts +11 -0
- package/dist/checkpoints.js +144 -0
- package/dist/compaction.js +9 -1
- package/dist/content.d.ts +102 -0
- package/dist/content.js +410 -0
- package/dist/contracts.d.ts +142 -2
- package/dist/event-multiplexer.d.ts +23 -0
- package/dist/event-multiplexer.js +136 -0
- package/dist/execution-policy.d.ts +28 -0
- package/dist/execution-policy.js +24 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.js +11 -4
- package/dist/input.js +11 -1
- package/dist/leases.d.ts +8 -0
- package/dist/leases.js +111 -0
- package/dist/node/agent-definitions.js +3 -5
- package/dist/node/config.d.ts +1 -0
- package/dist/node/config.js +5 -3
- package/dist/node/contribution-discovery.js +5 -8
- package/dist/node/session-store-jsonl.js +8 -5
- package/dist/node/settings.js +2 -2
- package/dist/node/trust.js +2 -4
- package/dist/observability.d.ts +3 -0
- package/dist/observability.js +18 -0
- package/dist/providers/media.d.ts +42 -0
- package/dist/providers/media.js +116 -0
- package/dist/providers/openai-compatible.js +18 -119
- package/dist/providers/openai-primitives.d.ts +9 -0
- package/dist/providers/openai-primitives.js +129 -0
- package/dist/providers/transport.d.ts +40 -0
- package/dist/providers/transport.js +221 -0
- package/dist/redaction.js +40 -13
- package/dist/resources.d.ts +5 -0
- package/dist/resources.js +4 -0
- package/dist/structured-output.d.ts +11 -0
- package/dist/structured-output.js +59 -0
- package/dist/testing/persistence-schema.d.ts +102 -0
- package/dist/testing/persistence-schema.js +457 -0
- package/dist/testing/provider-conformance.js +10 -1
- package/dist/testing/run-ledger-conformance.d.ts +33 -0
- package/dist/testing/run-ledger-conformance.js +172 -0
- package/dist/testing/session-store-conformance.d.ts +16 -0
- package/dist/testing/session-store-conformance.js +73 -0
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +29 -2
- package/docs/agent-events.md +13 -4
- package/docs/agent-loops.md +10 -4
- package/docs/agent-session-runtime.md +1 -0
- package/docs/cli-rpc.md +3 -0
- package/docs/coding-agent-tools.md +41 -7
- package/docs/coding-security.md +84 -0
- package/docs/credential-storage.md +177 -0
- package/docs/credentials-and-redaction.md +2 -1
- package/docs/database-persistence.md +44 -2
- package/docs/host-security.md +15 -1
- package/docs/index.md +28 -12
- package/docs/input-and-prompt-assembly.md +6 -5
- package/docs/mcp-tools.md +139 -0
- package/docs/middleware-hooks.md +2 -0
- package/docs/migration.md +21 -28
- package/docs/model-registry.md +5 -3
- package/docs/multimodal-content.md +148 -0
- package/docs/observability.md +163 -0
- package/docs/performance.md +40 -1
- package/docs/persistence-credentials-multimodality-primitives.md +303 -0
- package/docs/postgres-persistence.md +141 -0
- package/docs/provider-conformance.md +17 -0
- package/docs/provider-layer.md +1 -1
- package/docs/provider-primitives.md +281 -0
- package/docs/providers/kimi.md +1 -0
- package/docs/providers/neuralwatt.md +1 -0
- package/docs/providers/openai-compatible.md +2 -1
- package/docs/providers/openai.md +8 -1
- package/docs/providers/opencode-go.md +1 -0
- package/docs/providers/openrouter.md +1 -0
- package/docs/providers/zai.md +1 -0
- package/docs/public-contracts.md +9 -2
- package/docs/release-and-install.md +209 -25
- package/docs/resource-loading.md +14 -4
- package/docs/review-coverage-2026-07-14.md +260 -0
- package/docs/run-ledger-conformance.md +96 -0
- package/docs/runs-and-usage.md +2 -0
- package/docs/session-store-conformance.md +16 -0
- package/docs/session-stores-and-branching.md +1 -0
- package/docs/settings-auth-trust-security.md +2 -1
- package/docs/sqlite-persistence.md +122 -0
- package/docs/structured-output.md +9 -0
- package/docs/tool-conformance.md +1 -0
- package/docs/tool-execution-primitives.md +374 -0
- package/docs/tools.md +39 -1
- package/docs/workflow-orchestration-primitives.md +565 -0
- package/docs/workflow-tui-primitives.md +5 -0
- package/docs/workflows.md +219 -0
- package/package.json +33 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.4] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Shared bounded provider transport, OpenAI serialization/media helpers, native structured-output contracts, provider/tool timing metadata, and audio/file/document content capability checks.
|
|
15
|
+
- Generic checkpoint, atomic lease, and bounded event-multiplexer contracts plus persistence/run-ledger conformance helpers.
|
|
16
|
+
- Optional packages for JSON Schema tool validation, MCP, coding approval/sandboxing, OpenTelemetry, encrypted/keychain credentials, SQLite/PostgreSQL persistence, and bounded workflow orchestration.
|
|
17
|
+
- Manifest-only `base`, `code`, and `sdk` profiles; `prism-all` now transitively installs every first-party package.
|
|
18
|
+
- Workflow, multimodal, persistence/resume, provider telemetry, cache, and external-adapter examples.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Single-shot loops support ordered bounded parallel tools; `ToolDefinition.exclusive` serializes dangerous turns without reducing later concurrency.
|
|
23
|
+
- Provider requests, SSE/error bodies, media, schemas, event queues, checkpoints, and workflow fan-out/output use documented finite limits.
|
|
24
|
+
- Session/ledger writes preserve order and redact before persistence; revision-loop transcript ordering and OAuth abort polling are hardened.
|
|
25
|
+
- All first-party providers use shared bounded transport helpers and expose current structured-output, multimodal, caching, reasoning, telemetry, and retry behavior where supported.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- Added fail-closed schema/prototype-pollution, SSRF/media, SQL/tenant, path/shell approval, MCP result, credential-envelope, OAuth, redaction, and stale-worker fencing coverage.
|
|
30
|
+
- Optional privileged capabilities remain inactive until hosts explicitly register transports/tools, configure roots/credentials/databases, and approve execution.
|
|
31
|
+
|
|
10
32
|
## [0.0.3] - 2026-07-08
|
|
11
33
|
|
|
12
34
|
### Added
|
package/README.md
CHANGED
|
@@ -17,10 +17,9 @@ packages. Prism defines contracts, not apps.
|
|
|
17
17
|
OpenAI/OpenRouter use best-effort explicit cache hints, NeuralWatt uses
|
|
18
18
|
best-effort implicit prefix caching, and other providers have route/model-specific
|
|
19
19
|
or no cache-control support; see [docs/provider-caching.md](docs/provider-caching.md).
|
|
20
|
-
- **First-party packages**:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`@arnilo/prism-compaction-observational-memory`.
|
|
20
|
+
- **First-party packages**: six provider adapters, two compaction strategies,
|
|
21
|
+
coding tools/security, JSON Schema validation, MCP, workflows, OpenTelemetry,
|
|
22
|
+
encrypted credentials, SQLite/PostgreSQL persistence, and manifest-only install profiles.
|
|
24
23
|
- **Tools, context, skills**: host-owned tool registry with allow/deny filtering
|
|
25
24
|
and dispatch, context providers, and a skill registry with progressive
|
|
26
25
|
disclosure.
|
|
@@ -41,14 +40,16 @@ packages. Prism defines contracts, not apps.
|
|
|
41
40
|
npm install @arnilo/prism
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
First-party
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
First-party code packages are separate imports and require `@arnilo/prism` as
|
|
44
|
+
a non-optional peer. Install atomic packages directly or choose a manifest-only
|
|
45
|
+
family/profile; profiles install packages but expose no alias exports and activate nothing:
|
|
47
46
|
|
|
48
47
|
```bash
|
|
49
|
-
npm install @arnilo/prism @arnilo/prism-provider-openai
|
|
50
|
-
npm install @arnilo/prism
|
|
51
|
-
npm install @arnilo/prism-
|
|
48
|
+
npm install @arnilo/prism @arnilo/prism-provider-openai # core + one provider
|
|
49
|
+
npm install @arnilo/prism-base # core + compaction + validation
|
|
50
|
+
npm install @arnilo/prism-code @arnilo/prism-provider-openai # coding-agent profile
|
|
51
|
+
npm install @arnilo/prism-sdk @arnilo/prism-provider-openai # application profile
|
|
52
|
+
npm install @arnilo/prism-all # every first-party package
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
See [docs/release-and-install.md](docs/release-and-install.md) for install
|
|
@@ -111,8 +112,8 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
111
112
|
|
|
112
113
|
- [docs/index.md](docs/index.md) — navigational map of every public surface.
|
|
113
114
|
- The `examples/` directory holds compile-checked typed examples and runnable
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
offline demos covering providers, auth, tools, stores, compaction, structured
|
|
116
|
+
output, multimodality, workflows, CLI, and RPC.
|
|
116
117
|
|
|
117
118
|
## Packages
|
|
118
119
|
|
|
@@ -127,9 +128,21 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
127
128
|
| `@arnilo/prism-provider-neuralwatt` | NeuralWatt provider with implicit vLLM prefix caching |
|
|
128
129
|
| `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
|
|
129
130
|
| `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
|
|
130
|
-
| `@arnilo/prism-
|
|
131
|
-
| `@arnilo/prism-
|
|
132
|
-
| `@arnilo/prism-
|
|
131
|
+
| `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
|
|
132
|
+
| `@arnilo/prism-coding-security` | coding approval, containment, and sandbox adapters |
|
|
133
|
+
| `@arnilo/prism-tool-validator-json-schema` | bounded JSON Schema tool validation |
|
|
134
|
+
| `@arnilo/prism-mcp` | MCP client/tool bridge |
|
|
135
|
+
| `@arnilo/prism-workflows` | bounded DAG workflows and durable coordination |
|
|
136
|
+
| `@arnilo/prism-observability-opentelemetry` | optional OpenTelemetry adapter |
|
|
137
|
+
| `@arnilo/prism-credentials-node` | encrypted-file and keychain credentials |
|
|
138
|
+
| `@arnilo/prism-session-store-sqlite` | SQLite persistence/checkpoints/leases |
|
|
139
|
+
| `@arnilo/prism-session-store-postgres` | PostgreSQL persistence/checkpoints/leases |
|
|
140
|
+
| `@arnilo/prism-providers` | family: all 6 provider adapters |
|
|
141
|
+
| `@arnilo/prism-compaction` | family: both compaction strategies |
|
|
142
|
+
| `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
|
|
143
|
+
| `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
|
|
144
|
+
| `@arnilo/prism-sdk` | profile: base + workflows + MCP + credentials + OpenTelemetry |
|
|
145
|
+
| `@arnilo/prism-all` | every first-party package, including both persistence adapters |
|
|
133
146
|
|
|
134
147
|
## Scripts
|
|
135
148
|
|
|
@@ -142,8 +155,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
142
155
|
|
|
143
156
|
## Non-goals (v1)
|
|
144
157
|
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
- Encrypted/keychain credential storage — host-owned until a real app needs it.
|
|
158
|
+
- Privileged tools, MCP servers, telemetry, credentials, or databases activated by install — hosts explicitly configure and register every capability.
|
|
159
|
+
- Browser automation or interactive terminal UI — CLI/RPC and workflow control APIs only.
|
|
160
|
+
- Provider, credential, extension, or package auto-discovery.
|
|
161
|
+
- Core-owned database drivers, secret persistence, sandbox, or application policy — optional packages implement adapters over host-owned boundaries.
|
package/dist/agent-loops.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentLoopOptions, AgentLoopStrategy, ArtifactParser, ArtifactRepairer, ArtifactValidator } from "./contracts.js";
|
|
1
|
+
import type { AgentLoopOptions, AgentLoopStrategy, ArtifactParser, ArtifactRepairer, ArtifactValidator, LoopContext, ToolCallContent } from "./contracts.js";
|
|
2
2
|
export declare const singleShotLoop: AgentLoopStrategy;
|
|
3
3
|
export declare function isAgentLoopOptions(value: AgentLoopStrategy | AgentLoopOptions | undefined): value is AgentLoopOptions;
|
|
4
4
|
export declare function generateValidateReviseLoop(opts: {
|
|
@@ -7,6 +7,13 @@ export declare function generateValidateReviseLoop(opts: {
|
|
|
7
7
|
readonly repairer?: ArtifactRepairer<unknown>;
|
|
8
8
|
readonly maxRevisions?: number;
|
|
9
9
|
}): AgentLoopStrategy;
|
|
10
|
+
export declare function resolveToolConcurrency(options: {
|
|
11
|
+
loop?: AgentLoopStrategy | AgentLoopOptions;
|
|
12
|
+
}, config: {
|
|
13
|
+
loop?: AgentLoopStrategy | AgentLoopOptions;
|
|
14
|
+
}): number;
|
|
15
|
+
/** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
|
|
16
|
+
export declare function dispatchToolCallsInOrder(calls: readonly ToolCallContent[], ctx: LoopContext): Promise<void>;
|
|
10
17
|
export declare function resolveLoop(options: {
|
|
11
18
|
loop?: AgentLoopStrategy | AgentLoopOptions;
|
|
12
19
|
}, config: {
|
package/dist/agent-loops.js
CHANGED
|
@@ -18,13 +18,12 @@ export const singleShotLoop = {
|
|
|
18
18
|
name: "single-shot",
|
|
19
19
|
async run(ctx) {
|
|
20
20
|
let usage;
|
|
21
|
-
const toolResults = [];
|
|
22
21
|
let toolRounds = 0;
|
|
23
22
|
let nextInput = ctx.input;
|
|
24
23
|
for (let turn = 1;; turn += 1) {
|
|
25
24
|
throwIfAborted(ctx.signal);
|
|
26
25
|
ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
|
|
27
|
-
const request = await ctx.assemble(nextInput,
|
|
26
|
+
const request = await ctx.assemble(nextInput, undefined, turn);
|
|
28
27
|
throwIfAborted(ctx.signal);
|
|
29
28
|
const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
|
|
30
29
|
usage = turnUsage ?? usage;
|
|
@@ -40,12 +39,7 @@ export const singleShotLoop = {
|
|
|
40
39
|
if (calls.length === 0 || toolRounds >= ctx.maxToolRounds)
|
|
41
40
|
break;
|
|
42
41
|
toolRounds += 1;
|
|
43
|
-
|
|
44
|
-
const result = await ctx.dispatchToolCall(call);
|
|
45
|
-
toolResults.push(result);
|
|
46
|
-
await ctx.appendMessage(toolResultMessage(result));
|
|
47
|
-
throwIfAborted(ctx.signal);
|
|
48
|
-
}
|
|
42
|
+
await dispatchToolCallsInOrder(calls, ctx);
|
|
49
43
|
nextInput = [];
|
|
50
44
|
}
|
|
51
45
|
return usage;
|
|
@@ -73,6 +67,7 @@ export function generateValidateReviseLoop(opts) {
|
|
|
73
67
|
async run(ctx) {
|
|
74
68
|
let usage;
|
|
75
69
|
let nextInput = ctx.input;
|
|
70
|
+
let pendingHistory = [];
|
|
76
71
|
for (let turn = 1; turn <= max + 1; turn += 1) {
|
|
77
72
|
throwIfAborted(ctx.signal);
|
|
78
73
|
ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
|
|
@@ -80,6 +75,10 @@ export function generateValidateReviseLoop(opts) {
|
|
|
80
75
|
throwIfAborted(ctx.signal);
|
|
81
76
|
const { content, messageId, started, usage: turnUsage } = await ctx.generate(request);
|
|
82
77
|
usage = turnUsage ?? usage;
|
|
78
|
+
if (pendingHistory.length > 0) {
|
|
79
|
+
ctx.history.push(...pendingHistory);
|
|
80
|
+
pendingHistory = [];
|
|
81
|
+
}
|
|
83
82
|
if (turn === 1)
|
|
84
83
|
ctx.history.push(...ctx.inputMessages);
|
|
85
84
|
if (started) {
|
|
@@ -121,10 +120,9 @@ export function generateValidateReviseLoop(opts) {
|
|
|
121
120
|
ctx.emit({ type: "artifact_revision_started", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, failure: result });
|
|
122
121
|
const repair = await repairer(parsed.value, result, artifactCtx);
|
|
123
122
|
const repairMessages = inputMessages(repair).map((m) => ({ ...m, id: randomId("msg") }));
|
|
124
|
-
for (const message of repairMessages)
|
|
125
|
-
ctx.history.push(message);
|
|
123
|
+
for (const message of repairMessages)
|
|
126
124
|
await ctx.appendMessage(message);
|
|
127
|
-
|
|
125
|
+
pendingHistory = repairMessages;
|
|
128
126
|
nextInput = repairMessages;
|
|
129
127
|
continue;
|
|
130
128
|
}
|
|
@@ -135,6 +133,54 @@ export function generateValidateReviseLoop(opts) {
|
|
|
135
133
|
// ponytail: resolveLoop returns the loop to run. RunOptions.loop wins over
|
|
136
134
|
// AgentConfig.loop; default is singleShotLoop. generate-validate-revise maps
|
|
137
135
|
// its options to the factory; unknown strategy throws.
|
|
136
|
+
export function resolveToolConcurrency(options, config) {
|
|
137
|
+
const loop = options.loop ?? config.loop;
|
|
138
|
+
if (!loop || !isAgentLoopOptions(loop) || loop.strategy !== "single-shot")
|
|
139
|
+
return 1;
|
|
140
|
+
const value = loop.toolConcurrency;
|
|
141
|
+
if (value === undefined)
|
|
142
|
+
return 1;
|
|
143
|
+
if (!Number.isFinite(value) || value < 1)
|
|
144
|
+
return 1;
|
|
145
|
+
return Math.floor(value);
|
|
146
|
+
}
|
|
147
|
+
/** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
|
|
148
|
+
export async function dispatchToolCallsInOrder(calls, ctx) {
|
|
149
|
+
if (calls.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
const concurrency = calls.some((call) => ctx.isToolCallExclusive?.(call))
|
|
152
|
+
? 1
|
|
153
|
+
: Math.max(1, Math.min(ctx.toolConcurrency, calls.length));
|
|
154
|
+
if (concurrency === 1) {
|
|
155
|
+
for (const call of calls) {
|
|
156
|
+
const result = await ctx.dispatchToolCall(call);
|
|
157
|
+
await appendToolResultMessage(result, ctx);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const results = new Array(calls.length);
|
|
162
|
+
let nextIndex = 0;
|
|
163
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
164
|
+
for (;;) {
|
|
165
|
+
throwIfAborted(ctx.signal);
|
|
166
|
+
const index = nextIndex;
|
|
167
|
+
nextIndex += 1;
|
|
168
|
+
if (index >= calls.length)
|
|
169
|
+
return;
|
|
170
|
+
results[index] = await ctx.dispatchToolCall(calls[index]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
await Promise.all(workers);
|
|
174
|
+
for (const result of results) {
|
|
175
|
+
throwIfAborted(ctx.signal);
|
|
176
|
+
await appendToolResultMessage(result, ctx);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function appendToolResultMessage(result, ctx) {
|
|
180
|
+
const message = toolResultMessage(result);
|
|
181
|
+
ctx.history.push(message);
|
|
182
|
+
await ctx.appendMessage(message);
|
|
183
|
+
}
|
|
138
184
|
export function resolveLoop(options, config) {
|
|
139
185
|
const loop = options.loop ?? config.loop;
|
|
140
186
|
if (!loop)
|
package/dist/agents.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { resolveLoop } from "./agent-loops.js";
|
|
1
|
+
import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
|
|
2
|
+
import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
|
|
2
3
|
import { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
|
|
3
4
|
import { assembleProviderInput } from "./input.js";
|
|
4
5
|
import { providerToolCallDeltaContent, reconstructToolCallDeltas } from "./provider-events.js";
|
|
5
|
-
import { createProviderRequestPolicyChain,
|
|
6
|
+
import { createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult } from "./provider-request-policy.js";
|
|
7
|
+
import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
|
|
6
8
|
import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
|
|
7
9
|
import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
|
|
8
10
|
import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
|
|
@@ -34,7 +36,9 @@ class RuntimeAgentSession {
|
|
|
34
36
|
activeLedger;
|
|
35
37
|
activeOwnership;
|
|
36
38
|
activeIdempotencyKey;
|
|
37
|
-
|
|
39
|
+
activeLoopTurn = 1;
|
|
40
|
+
ledgerChain = Promise.resolve();
|
|
41
|
+
ledgerFailure;
|
|
38
42
|
constructor(config) {
|
|
39
43
|
this.id = config.id ?? randomId("session");
|
|
40
44
|
this.agent = config.agent;
|
|
@@ -101,12 +105,15 @@ class RuntimeAgentSession {
|
|
|
101
105
|
// ponytail: skill context after host context; no per-skill token budget yet.
|
|
102
106
|
...activeSkills.flatMap((skill) => skill.context ?? []),
|
|
103
107
|
];
|
|
104
|
-
const providerOptions =
|
|
108
|
+
const providerOptions = resolveRunProviderOptions(options, this.agent.config);
|
|
109
|
+
assertStructuredOutputRequestSupported(options.model ?? this.agent.config.model, providerOptions);
|
|
105
110
|
const validate = options.validate ?? this.agent.config.validator;
|
|
106
111
|
// ponytail: RunOptions.instructionInjectors overrides AgentConfig.instructionInjectors (mirrors validate/loop).
|
|
107
112
|
const instructionInjectors = options.instructionInjectors ?? this.agent.config.instructionInjectors ?? [];
|
|
108
113
|
const inputLayout = options.inputLayout ?? this.agent.config.inputLayout;
|
|
109
114
|
const loop = resolveLoop(options, this.agent.config);
|
|
115
|
+
const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
|
|
116
|
+
this.activeLoopTurn = 1;
|
|
110
117
|
// ponytail: LoopContext binds existing private helpers; loop orchestrates only.
|
|
111
118
|
const ctx = {
|
|
112
119
|
sessionId: this.id,
|
|
@@ -117,6 +124,7 @@ class RuntimeAgentSession {
|
|
|
117
124
|
input,
|
|
118
125
|
inputMessages,
|
|
119
126
|
maxToolRounds,
|
|
127
|
+
toolConcurrency,
|
|
120
128
|
assemble: async (nextInput, toolResults, turn) => assembleProviderInput({
|
|
121
129
|
model: options.model ?? this.agent.config.model,
|
|
122
130
|
input: nextInput,
|
|
@@ -144,8 +152,9 @@ class RuntimeAgentSession {
|
|
|
144
152
|
generate: async (request) => {
|
|
145
153
|
const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
|
|
146
154
|
const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
|
|
147
|
-
return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets);
|
|
155
|
+
return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn);
|
|
148
156
|
},
|
|
157
|
+
isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
|
|
149
158
|
dispatchToolCall: (call) => dispatchToolCall({
|
|
150
159
|
call,
|
|
151
160
|
registry,
|
|
@@ -160,7 +169,11 @@ class RuntimeAgentSession {
|
|
|
160
169
|
validate,
|
|
161
170
|
}),
|
|
162
171
|
appendMessage: (message) => this.appendMessage(message, runId),
|
|
163
|
-
emit: (event) =>
|
|
172
|
+
emit: (event) => {
|
|
173
|
+
if (event.type === "turn_started")
|
|
174
|
+
this.activeLoopTurn = event.turn;
|
|
175
|
+
this.emit(event);
|
|
176
|
+
},
|
|
164
177
|
};
|
|
165
178
|
const usage = await loop.run(ctx);
|
|
166
179
|
if (usage && this.activeLedger) {
|
|
@@ -301,7 +314,17 @@ class RuntimeAgentSession {
|
|
|
301
314
|
redacted: Boolean(this.activeRedactor),
|
|
302
315
|
...this.activeOwnership,
|
|
303
316
|
};
|
|
304
|
-
this.
|
|
317
|
+
const ledger = this.activeLedger;
|
|
318
|
+
this.ledgerChain = this.ledgerChain.then(async () => {
|
|
319
|
+
if (this.ledgerFailure)
|
|
320
|
+
return;
|
|
321
|
+
try {
|
|
322
|
+
await ledger.appendEvent(redactRunLedgerRecord(record, this.activeRedactor));
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.ledgerFailure = error;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
closeSubscribers() {
|
|
@@ -310,19 +333,20 @@ class RuntimeAgentSession {
|
|
|
310
333
|
this.subscribers.clear();
|
|
311
334
|
}
|
|
312
335
|
async drainLedger() {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
this.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
336
|
+
await this.ledgerChain;
|
|
337
|
+
const failure = this.ledgerFailure;
|
|
338
|
+
this.ledgerChain = Promise.resolve();
|
|
339
|
+
this.ledgerFailure = undefined;
|
|
340
|
+
if (failure)
|
|
341
|
+
throw failure;
|
|
342
|
+
}
|
|
343
|
+
async generateWithRetry(request, runId, options, signal, requestSecrets = [], turn = 1) {
|
|
320
344
|
const retry = mergeRetry(this.agent.config.retry, options.retry);
|
|
321
345
|
const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
|
|
322
346
|
const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
|
|
323
347
|
for (let attempt = 1;; attempt += 1) {
|
|
324
348
|
try {
|
|
325
|
-
return await this.generateProviderTurn(request, runId, signal, secrets);
|
|
349
|
+
return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt);
|
|
326
350
|
}
|
|
327
351
|
catch (error) {
|
|
328
352
|
const failure = error instanceof ProviderTurnFailure ? error : undefined;
|
|
@@ -341,7 +365,17 @@ class RuntimeAgentSession {
|
|
|
341
365
|
}
|
|
342
366
|
}
|
|
343
367
|
}
|
|
344
|
-
async generateProviderTurn(request, runId, signal, secrets = []) {
|
|
368
|
+
async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1) {
|
|
369
|
+
const startedAt = performance.now();
|
|
370
|
+
const providerId = this.activeProvider?.id ?? request.model.provider;
|
|
371
|
+
const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
|
|
372
|
+
this.emit({
|
|
373
|
+
type: "provider_turn_started",
|
|
374
|
+
sessionId: this.id,
|
|
375
|
+
runId,
|
|
376
|
+
turn,
|
|
377
|
+
metadata: buildMetadata(),
|
|
378
|
+
});
|
|
345
379
|
const content = [];
|
|
346
380
|
const calls = [];
|
|
347
381
|
const toolDeltas = [];
|
|
@@ -399,12 +433,31 @@ class RuntimeAgentSession {
|
|
|
399
433
|
calls.push(call);
|
|
400
434
|
this.emit({ type: "message_delta", sessionId: this.id, runId, content: call });
|
|
401
435
|
}
|
|
436
|
+
const latencyMs = Math.round(performance.now() - startedAt);
|
|
437
|
+
this.emit({
|
|
438
|
+
type: "provider_turn_finished",
|
|
439
|
+
sessionId: this.id,
|
|
440
|
+
runId,
|
|
441
|
+
turn,
|
|
442
|
+
metadata: buildMetadata({ latencyMs }),
|
|
443
|
+
usage,
|
|
444
|
+
});
|
|
402
445
|
return { content, calls, messageId, started, usage };
|
|
403
446
|
}
|
|
404
447
|
catch (error) {
|
|
448
|
+
const latencyMs = Math.round(performance.now() - startedAt);
|
|
449
|
+
const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
|
|
450
|
+
this.emit({
|
|
451
|
+
type: "provider_turn_finished",
|
|
452
|
+
sessionId: this.id,
|
|
453
|
+
runId,
|
|
454
|
+
turn,
|
|
455
|
+
metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
|
|
456
|
+
error: info,
|
|
457
|
+
});
|
|
405
458
|
if (error instanceof ProviderTurnFailure)
|
|
406
459
|
throw error;
|
|
407
|
-
throw new ProviderTurnFailure(
|
|
460
|
+
throw new ProviderTurnFailure(info, started);
|
|
408
461
|
}
|
|
409
462
|
}
|
|
410
463
|
async applyProviderRequestPolicies(request, runId, options, metadata, signal) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CheckpointStore } from "./contracts.js";
|
|
2
|
+
export declare const CHECKPOINT_CONFLICT_CODE = "ERR_PRISM_CHECKPOINT_CONFLICT";
|
|
3
|
+
export declare class CheckpointConflictError extends Error {
|
|
4
|
+
readonly code = "ERR_PRISM_CHECKPOINT_CONFLICT";
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface MemoryCheckpointStoreOptions {
|
|
8
|
+
readonly maxPageSize?: number;
|
|
9
|
+
}
|
|
10
|
+
/** In-process reference implementation of the generic checkpoint contract. */
|
|
11
|
+
export declare function createMemoryCheckpointStore(options?: MemoryCheckpointStoreOptions): CheckpointStore;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const CHECKPOINT_CONFLICT_CODE = "ERR_PRISM_CHECKPOINT_CONFLICT";
|
|
2
|
+
export class CheckpointConflictError extends Error {
|
|
3
|
+
code = CHECKPOINT_CONFLICT_CODE;
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CheckpointConflictError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** In-process reference implementation of the generic checkpoint contract. */
|
|
10
|
+
export function createMemoryCheckpointStore(options = {}) {
|
|
11
|
+
const records = new Map();
|
|
12
|
+
const maxPageSize = Math.max(1, options.maxPageSize ?? 500);
|
|
13
|
+
return {
|
|
14
|
+
async saveCheckpoint(input) {
|
|
15
|
+
throwIfAborted(input.signal);
|
|
16
|
+
assertKey(input);
|
|
17
|
+
if (!Number.isSafeInteger(input.version) || input.version < 1) {
|
|
18
|
+
throw new CheckpointConflictError("Checkpoint version must be a positive safe integer");
|
|
19
|
+
}
|
|
20
|
+
const id = recordKey(input);
|
|
21
|
+
const existing = records.get(id);
|
|
22
|
+
if (existing)
|
|
23
|
+
assertOwnership(input, existing);
|
|
24
|
+
if (input.expectedVersion !== undefined && input.expectedVersion !== (existing?.version ?? 0)) {
|
|
25
|
+
throw new CheckpointConflictError(`Checkpoint compare-and-swap failed (expected ${input.expectedVersion}, current ${existing?.version ?? 0})`);
|
|
26
|
+
}
|
|
27
|
+
if (existing && input.version <= existing.version) {
|
|
28
|
+
throw new CheckpointConflictError(`Stale checkpoint version ${input.version} (current ${existing.version})`);
|
|
29
|
+
}
|
|
30
|
+
if (existing?.fencingToken !== undefined && (input.fencingToken === undefined || input.fencingToken < existing.fencingToken)) {
|
|
31
|
+
throw new CheckpointConflictError(`Stale checkpoint fencing token ${input.fencingToken ?? "missing"} (current ${existing.fencingToken})`);
|
|
32
|
+
}
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const value = cloneJson(input.value, "Checkpoint value");
|
|
35
|
+
const metadata = input.metadata === undefined ? undefined : cloneJson(input.metadata, "Checkpoint metadata");
|
|
36
|
+
const record = {
|
|
37
|
+
namespace: input.namespace,
|
|
38
|
+
key: input.key,
|
|
39
|
+
version: input.version,
|
|
40
|
+
...(input.fencingToken === undefined ? {} : { fencingToken: input.fencingToken }),
|
|
41
|
+
value,
|
|
42
|
+
...(input.category === undefined ? {} : { category: input.category }),
|
|
43
|
+
...ownership(input),
|
|
44
|
+
createdAt: existing?.createdAt ?? now,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
47
|
+
};
|
|
48
|
+
records.set(id, record);
|
|
49
|
+
return record;
|
|
50
|
+
},
|
|
51
|
+
async loadCheckpoint(input) {
|
|
52
|
+
throwIfAborted(input.signal);
|
|
53
|
+
assertKey(input);
|
|
54
|
+
const record = records.get(recordKey(input));
|
|
55
|
+
if (!record)
|
|
56
|
+
return null;
|
|
57
|
+
assertOwnership(input, record);
|
|
58
|
+
return record;
|
|
59
|
+
},
|
|
60
|
+
async listCheckpoints(query = {}) {
|
|
61
|
+
throwIfAborted(query.signal);
|
|
62
|
+
const offset = decodeCursor(query.cursor);
|
|
63
|
+
const limit = Math.min(Math.max(1, query.limit ?? 100), maxPageSize);
|
|
64
|
+
const categories = query.category === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: new Set(Array.isArray(query.category) ? query.category : [query.category]);
|
|
67
|
+
const items = [...records.values()]
|
|
68
|
+
.filter((record) => query.namespace === undefined || record.namespace === query.namespace)
|
|
69
|
+
.filter((record) => query.keyPrefix === undefined || record.key.startsWith(query.keyPrefix))
|
|
70
|
+
.filter((record) => categories === undefined || (record.category !== undefined && categories.has(record.category)))
|
|
71
|
+
.filter((record) => ownershipFilterMatches(query, record))
|
|
72
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.key.localeCompare(b.key));
|
|
73
|
+
const page = items.slice(offset, offset + limit);
|
|
74
|
+
const next = offset + page.length;
|
|
75
|
+
return {
|
|
76
|
+
items: page,
|
|
77
|
+
...(next < items.length ? { nextCursor: String(next) } : {}),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
async deleteCheckpoint(input) {
|
|
81
|
+
throwIfAborted(input.signal);
|
|
82
|
+
assertKey(input);
|
|
83
|
+
const id = recordKey(input);
|
|
84
|
+
const record = records.get(id);
|
|
85
|
+
if (!record)
|
|
86
|
+
return false;
|
|
87
|
+
assertOwnership(input, record);
|
|
88
|
+
return records.delete(id);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function recordKey(input) {
|
|
93
|
+
return `${input.namespace}\0${input.key}`;
|
|
94
|
+
}
|
|
95
|
+
function assertKey(input) {
|
|
96
|
+
if (!input.namespace || !input.key)
|
|
97
|
+
throw new CheckpointConflictError("Checkpoint namespace and key are required");
|
|
98
|
+
}
|
|
99
|
+
function ownership(input) {
|
|
100
|
+
return {
|
|
101
|
+
...(input.tenantId === undefined ? {} : { tenantId: input.tenantId }),
|
|
102
|
+
...(input.accountId === undefined ? {} : { accountId: input.accountId }),
|
|
103
|
+
...(input.userId === undefined ? {} : { userId: input.userId }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function ownershipMatches(expected, actual) {
|
|
107
|
+
return expected.tenantId === actual.tenantId
|
|
108
|
+
&& expected.accountId === actual.accountId
|
|
109
|
+
&& expected.userId === actual.userId;
|
|
110
|
+
}
|
|
111
|
+
function ownershipFilterMatches(expected, actual) {
|
|
112
|
+
return (expected.tenantId === undefined || expected.tenantId === actual.tenantId)
|
|
113
|
+
&& (expected.accountId === undefined || expected.accountId === actual.accountId)
|
|
114
|
+
&& (expected.userId === undefined || expected.userId === actual.userId);
|
|
115
|
+
}
|
|
116
|
+
function assertOwnership(expected, actual) {
|
|
117
|
+
if (!ownershipMatches(expected, actual)) {
|
|
118
|
+
throw new CheckpointConflictError("Checkpoint ownership mismatch");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function cloneJson(value, label) {
|
|
122
|
+
try {
|
|
123
|
+
const encoded = JSON.stringify(value);
|
|
124
|
+
if (encoded === undefined)
|
|
125
|
+
throw new Error("not JSON serializable");
|
|
126
|
+
return JSON.parse(encoded);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
throw new CheckpointConflictError(`${label} must be JSON serializable: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function decodeCursor(cursor) {
|
|
133
|
+
if (cursor === undefined)
|
|
134
|
+
return 0;
|
|
135
|
+
const value = Number(cursor);
|
|
136
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
137
|
+
throw new CheckpointConflictError("Invalid checkpoint cursor");
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function throwIfAborted(signal) {
|
|
141
|
+
if (signal?.aborted)
|
|
142
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=checkpoints.js.map
|
package/dist/compaction.js
CHANGED
|
@@ -59,7 +59,15 @@ function blockText(block) {
|
|
|
59
59
|
return `[tool_call_delta ${block.name ?? block.index}]`;
|
|
60
60
|
if (block.type === "tool_result")
|
|
61
61
|
return `[tool_result ${block.name}]`;
|
|
62
|
-
|
|
62
|
+
if (block.type === "image")
|
|
63
|
+
return "[image]";
|
|
64
|
+
if (block.type === "audio")
|
|
65
|
+
return "[audio]";
|
|
66
|
+
if (block.type === "file")
|
|
67
|
+
return "[file]";
|
|
68
|
+
if (block.type === "document")
|
|
69
|
+
return "[document]";
|
|
70
|
+
return "[content]";
|
|
63
71
|
}
|
|
64
72
|
function truncate(text, max) {
|
|
65
73
|
if (max === 0 || text.length <= max)
|