@arnilo/prism 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +34 -10
  3. package/dist/agents.js +146 -19
  4. package/dist/cli-init.d.ts +41 -0
  5. package/dist/cli-init.js +390 -0
  6. package/dist/cli-runner.d.ts +7 -1
  7. package/dist/cli-runner.js +13 -1
  8. package/dist/content.d.ts +19 -0
  9. package/dist/content.js +197 -69
  10. package/dist/contracts.d.ts +94 -9
  11. package/dist/contracts.js +8 -0
  12. package/dist/feedback.d.ts +48 -0
  13. package/dist/feedback.js +230 -0
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.js +4 -3
  16. package/dist/providers/media.d.ts +3 -1
  17. package/dist/providers/media.js +11 -1
  18. package/dist/testing/feedback.d.ts +6 -0
  19. package/dist/testing/feedback.js +37 -0
  20. package/dist/testing/persistence-schema.d.ts +3 -3
  21. package/dist/testing/persistence-schema.js +32 -2
  22. package/dist/testing/run-ledger-conformance.js +7 -1
  23. package/docs/a2a.md +73 -0
  24. package/docs/agent-events.md +4 -6
  25. package/docs/agent-loops.md +1 -1
  26. package/docs/agent-session-runtime.md +14 -16
  27. package/docs/cli-rpc.md +35 -7
  28. package/docs/coding-agent-tools.md +2 -2
  29. package/docs/coding-security.md +7 -3
  30. package/docs/compaction-observational-memory.md +2 -0
  31. package/docs/context-and-skills.md +1 -0
  32. package/docs/credentials-and-redaction.md +2 -2
  33. package/docs/database-persistence.md +9 -6
  34. package/docs/evaluations.md +122 -0
  35. package/docs/extensions.md +2 -2
  36. package/docs/host-security.md +20 -3
  37. package/docs/index.md +29 -17
  38. package/docs/mcp-tools.md +49 -4
  39. package/docs/migration.md +33 -3
  40. package/docs/multimodal-content.md +14 -6
  41. package/docs/observability.md +14 -6
  42. package/docs/performance.md +209 -0
  43. package/docs/postgres-persistence.md +6 -4
  44. package/docs/provider-conformance.md +1 -0
  45. package/docs/provider-packages.md +2 -0
  46. package/docs/providers/ai-sdk.md +113 -0
  47. package/docs/public-contracts.md +6 -5
  48. package/docs/rag.md +113 -0
  49. package/docs/release-and-install.md +100 -77
  50. package/docs/review-coverage-2026-07-15.md +193 -0
  51. package/docs/runs-and-usage.md +41 -4
  52. package/docs/server.md +139 -0
  53. package/docs/settings-auth-trust-security.md +5 -5
  54. package/docs/sqlite-persistence.md +4 -3
  55. package/docs/supervisors.md +71 -0
  56. package/docs/workflow-orchestration-primitives.md +19 -3
  57. package/docs/workflows.md +97 -23
  58. package/docs/working-and-semantic-memory.md +169 -0
  59. package/package.json +12 -2
  60. package/templates/init/README.md.tmpl +28 -0
  61. package/templates/init/env.example.tmpl +1 -0
  62. package/templates/init/gitignore.tmpl +11 -0
  63. package/templates/init/optional/evals-example.ts.tmpl +17 -0
  64. package/templates/init/optional/workflows-example.ts.tmpl +27 -0
  65. package/templates/init/package.json.tmpl +22 -0
  66. package/templates/init/providers.json +76 -0
  67. package/templates/init/src/agent.ts.tmpl +10 -0
  68. package/templates/init/src/index.ts.tmpl +12 -0
  69. package/templates/init/src/tests/agent.test.ts.tmpl +24 -0
  70. package/templates/init/tsconfig.json.tmpl +15 -0
@@ -0,0 +1,230 @@
1
+ export const DEFAULT_MAX_FEEDBACK_COMMENT_BYTES = 4096;
2
+ export const HARD_MAX_FEEDBACK_COMMENT_BYTES = 16384;
3
+ export const DEFAULT_MAX_FEEDBACK_TAGS = 16;
4
+ export const HARD_MAX_FEEDBACK_TAGS = 64;
5
+ export const DEFAULT_MAX_FEEDBACK_LINKS = 16;
6
+ export const HARD_MAX_FEEDBACK_LINKS = 64;
7
+ export const DEFAULT_MAX_FEEDBACK_METADATA_BYTES = 16384;
8
+ export const HARD_MAX_FEEDBACK_METADATA_BYTES = 65536;
9
+ export const DEFAULT_FEEDBACK_PAGE_SIZE = 100;
10
+ export const HARD_FEEDBACK_PAGE_SIZE = 500;
11
+ export const MAX_FEEDBACK_TAG_LENGTH = 64;
12
+ export const MAX_FEEDBACK_ID_LENGTH = 128;
13
+ export class RunFeedbackError extends Error {
14
+ code;
15
+ constructor(message, code = "ERR_PRISM_RUN_FEEDBACK") {
16
+ super(message);
17
+ this.name = "RunFeedbackError";
18
+ this.code = code;
19
+ }
20
+ }
21
+ /** Validate, ownership-check, redact, bound, and freeze one feedback record. */
22
+ export async function prepareRunFeedback(input, options) {
23
+ input.signal?.throwIfAborted();
24
+ const ownership = requireOwnership(input);
25
+ const id = requireId(input.id, "feedback id");
26
+ const runId = requireId(input.runId, "runId");
27
+ const run = await options.resolveRun({ runId, ownership, signal: input.signal });
28
+ input.signal?.throwIfAborted();
29
+ if (!run || run.runId !== runId || !sameOwnership(ownership, run)) {
30
+ throw new RunFeedbackError("Run not found", "ERR_PRISM_RUN_FEEDBACK_RUN_NOT_FOUND");
31
+ }
32
+ if (input.sessionId !== undefined && input.sessionId !== run.sessionId) {
33
+ throw new RunFeedbackError("Run not found", "ERR_PRISM_RUN_FEEDBACK_RUN_NOT_FOUND");
34
+ }
35
+ if (input.traceId !== undefined && run.traceId !== undefined && input.traceId !== run.traceId) {
36
+ throw new RunFeedbackError("Run not found", "ERR_PRISM_RUN_FEEDBACK_RUN_NOT_FOUND");
37
+ }
38
+ if (input.comment !== undefined && !input.comment.trim()) {
39
+ throw new RunFeedbackError("comment must not be empty", "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
40
+ }
41
+ if (input.rating !== undefined && (!Number.isFinite(input.rating) || input.rating < -1 || input.rating > 1)) {
42
+ throw new RunFeedbackError("rating must be a finite number in [-1, 1]", "ERR_PRISM_RUN_FEEDBACK_BOUNDS");
43
+ }
44
+ const maxCommentBytes = boundedLimit(options.maxCommentBytes, DEFAULT_MAX_FEEDBACK_COMMENT_BYTES, HARD_MAX_FEEDBACK_COMMENT_BYTES, "maxCommentBytes");
45
+ const maxTags = boundedLimit(options.maxTags, DEFAULT_MAX_FEEDBACK_TAGS, HARD_MAX_FEEDBACK_TAGS, "maxTags");
46
+ const maxLinks = boundedLimit(options.maxLinks, DEFAULT_MAX_FEEDBACK_LINKS, HARD_MAX_FEEDBACK_LINKS, "maxLinks");
47
+ const maxMetadataBytes = boundedLimit(options.maxMetadataBytes, DEFAULT_MAX_FEEDBACK_METADATA_BYTES, HARD_MAX_FEEDBACK_METADATA_BYTES, "maxMetadataBytes");
48
+ const tags = normalizeList(input.tags, maxTags, MAX_FEEDBACK_TAG_LENGTH, "tag");
49
+ const scorerIds = normalizeList(input.scorerIds, maxLinks, MAX_FEEDBACK_ID_LENGTH, "scorer id", true);
50
+ const evaluationIds = normalizeList(input.evaluationIds, maxLinks, MAX_FEEDBACK_ID_LENGTH, "evaluation id", true);
51
+ if (input.rating === undefined && input.comment === undefined && tags.length === 0 && scorerIds.length === 0 && evaluationIds.length === 0) {
52
+ throw new RunFeedbackError("feedback requires rating, comment, tag, scorer, or evaluation", "ERR_PRISM_RUN_FEEDBACK_EMPTY");
53
+ }
54
+ const redact = (value) => options.redactor ? options.redactor.redact(value) : value;
55
+ const comment = input.comment === undefined ? undefined : redact(input.comment);
56
+ const safeTags = Object.freeze(normalizeList(redact(tags), maxTags, MAX_FEEDBACK_TAG_LENGTH, "tag"));
57
+ const metadata = input.metadata === undefined ? undefined : redact(input.metadata);
58
+ assertBytes(comment, maxCommentBytes, "comment");
59
+ assertBytes(metadata, maxMetadataBytes, "metadata");
60
+ const createdAt = input.createdAt ?? new Date().toISOString();
61
+ if (!Number.isFinite(Date.parse(createdAt)))
62
+ throw new RunFeedbackError("createdAt must be an ISO timestamp", "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
63
+ return Object.freeze({
64
+ id,
65
+ runId,
66
+ sessionId: run.sessionId,
67
+ traceId: input.traceId ?? run.traceId,
68
+ rating: input.rating,
69
+ comment,
70
+ tags: safeTags,
71
+ scorerIds: Object.freeze(scorerIds),
72
+ evaluationIds: Object.freeze(evaluationIds),
73
+ createdAt,
74
+ createdBy: input.createdBy === undefined ? undefined : requireId(input.createdBy, "createdBy"),
75
+ metadata: metadata === undefined ? undefined : cloneFrozenJsonObject(metadata),
76
+ ...ownership,
77
+ });
78
+ }
79
+ /** In-memory implementation with the same validation/ownership semantics as durable adapters. */
80
+ export function createMemoryRunFeedbackStore(options) {
81
+ const records = new Map();
82
+ for (const record of options.initial ?? [])
83
+ records.set(record.id, freezeRecord(record));
84
+ const maxPageSize = boundedLimit(options.maxPageSize, DEFAULT_FEEDBACK_PAGE_SIZE, HARD_FEEDBACK_PAGE_SIZE, "maxPageSize");
85
+ return {
86
+ async append(input) {
87
+ if (records.has(input.id))
88
+ throw new RunFeedbackError("Duplicate feedback id", "ERR_PRISM_RUN_FEEDBACK_DUPLICATE");
89
+ const record = await prepareRunFeedback(input, options);
90
+ records.set(record.id, record);
91
+ return record;
92
+ },
93
+ async query(query) {
94
+ query.signal?.throwIfAborted();
95
+ const ownership = requireOwnership(query);
96
+ const limit = pageLimit(query.limit, maxPageSize);
97
+ const order = query.order === "desc" ? -1 : 1;
98
+ const sorted = [...records.values()]
99
+ .filter((record) => sameOwnership(ownership, record) && matchesQuery(record, query))
100
+ .sort((a, b) => order * (a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)));
101
+ const start = query.cursor ? sorted.findIndex((record) => record.id === query.cursor) + 1 : 0;
102
+ if (query.cursor && start === 0)
103
+ throw new RunFeedbackError("Unknown feedback cursor", "ERR_PRISM_RUN_FEEDBACK_CURSOR");
104
+ const items = sorted.slice(start, start + limit);
105
+ return { items, nextCursor: start + items.length < sorted.length ? items.at(-1)?.id : undefined, total: sorted.length };
106
+ },
107
+ async delete(input) {
108
+ input.signal?.throwIfAborted();
109
+ const ownership = requireOwnership(input);
110
+ const current = records.get(input.id);
111
+ if (!current || !sameOwnership(ownership, current))
112
+ return false;
113
+ return records.delete(input.id);
114
+ },
115
+ };
116
+ }
117
+ export function requireRunFeedbackOwnership(input) {
118
+ return requireOwnership(input);
119
+ }
120
+ export function runFeedbackPageLimit(limit, maximum = HARD_FEEDBACK_PAGE_SIZE) {
121
+ return pageLimit(limit, maximum);
122
+ }
123
+ function requireOwnership(input) {
124
+ if (!input.tenantId?.trim()
125
+ || (input.accountId !== undefined && !input.accountId.trim())
126
+ || (input.userId !== undefined && !input.userId.trim())
127
+ || (!input.accountId && !input.userId)) {
128
+ throw new RunFeedbackError("tenantId and non-empty accountId or userId are required", "ERR_PRISM_RUN_FEEDBACK_OWNERSHIP");
129
+ }
130
+ return { tenantId: input.tenantId, accountId: input.accountId, userId: input.userId };
131
+ }
132
+ function sameOwnership(expected, actual) {
133
+ return expected.tenantId === actual.tenantId
134
+ && expected.accountId === actual.accountId
135
+ && expected.userId === actual.userId;
136
+ }
137
+ function requireId(value, label) {
138
+ if (!value || value.length > MAX_FEEDBACK_ID_LENGTH || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value)) {
139
+ throw new RunFeedbackError(`${label} is invalid`, "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
140
+ }
141
+ return value;
142
+ }
143
+ function normalizeList(values, maximum, maxLength, label, ids = false) {
144
+ if (!values)
145
+ return [];
146
+ if (values.length > maximum)
147
+ throw new RunFeedbackError(`Too many ${label}s`, "ERR_PRISM_RUN_FEEDBACK_BOUNDS");
148
+ const result = values.map((value) => {
149
+ if (typeof value !== "string" || !value.trim() || value.length > maxLength || /[\r\n]/.test(value)) {
150
+ throw new RunFeedbackError(`${label} is invalid`, "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
151
+ }
152
+ return ids ? requireId(value, label) : value;
153
+ });
154
+ if (new Set(result).size !== result.length)
155
+ throw new RunFeedbackError(`Duplicate ${label}`, "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
156
+ return result;
157
+ }
158
+ function boundedLimit(value, fallback, hard, label) {
159
+ const resolved = value ?? fallback;
160
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > hard) {
161
+ throw new RunFeedbackError(`${label} must be an integer in [1, ${hard}]`, "ERR_PRISM_RUN_FEEDBACK_BOUNDS");
162
+ }
163
+ return resolved;
164
+ }
165
+ function pageLimit(value, maximum) {
166
+ if (value === undefined)
167
+ return Math.min(DEFAULT_FEEDBACK_PAGE_SIZE, maximum);
168
+ if (!Number.isSafeInteger(value) || value < 1)
169
+ throw new RunFeedbackError("limit must be a positive integer", "ERR_PRISM_RUN_FEEDBACK_BOUNDS");
170
+ return Math.min(value, maximum);
171
+ }
172
+ function assertBytes(value, maximum, label) {
173
+ if (value === undefined)
174
+ return;
175
+ let encoded;
176
+ try {
177
+ encoded = typeof value === "string" ? value : JSON.stringify(value);
178
+ }
179
+ catch {
180
+ throw new RunFeedbackError(`${label} must be JSON serializable`, "ERR_PRISM_RUN_FEEDBACK_VALIDATION");
181
+ }
182
+ if (new TextEncoder().encode(encoded).byteLength > maximum) {
183
+ throw new RunFeedbackError(`${label} exceeds ${maximum} bytes`, "ERR_PRISM_RUN_FEEDBACK_BOUNDS");
184
+ }
185
+ }
186
+ function matchesQuery(record, query) {
187
+ if (query.runId !== undefined && record.runId !== query.runId)
188
+ return false;
189
+ if (query.sessionId !== undefined && record.sessionId !== query.sessionId)
190
+ return false;
191
+ if (query.traceId !== undefined && record.traceId !== query.traceId)
192
+ return false;
193
+ if (query.rating !== undefined && record.rating !== query.rating)
194
+ return false;
195
+ if (query.scorerId !== undefined && !record.scorerIds.includes(query.scorerId))
196
+ return false;
197
+ if (query.evaluationId !== undefined && !record.evaluationIds.includes(query.evaluationId))
198
+ return false;
199
+ if (query.tag !== undefined && !record.tags.includes(query.tag))
200
+ return false;
201
+ if (query.fromCreatedAt !== undefined && record.createdAt < query.fromCreatedAt)
202
+ return false;
203
+ if (query.toCreatedAt !== undefined && record.createdAt > query.toCreatedAt)
204
+ return false;
205
+ return true;
206
+ }
207
+ function freezeRecord(record) {
208
+ return Object.freeze({
209
+ ...record,
210
+ tags: Object.freeze([...record.tags]),
211
+ scorerIds: Object.freeze([...record.scorerIds]),
212
+ evaluationIds: Object.freeze([...record.evaluationIds]),
213
+ metadata: record.metadata ? cloneFrozenJsonObject(record.metadata) : undefined,
214
+ });
215
+ }
216
+ function cloneFrozenJsonObject(value) {
217
+ const cloned = JSON.parse(JSON.stringify(value));
218
+ if (!cloned || typeof cloned !== "object" || Array.isArray(cloned))
219
+ throw new RunFeedbackError("metadata must be a JSON object");
220
+ return deepFreeze(cloned);
221
+ }
222
+ function deepFreeze(value) {
223
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
224
+ for (const child of Object.values(value))
225
+ deepFreeze(child);
226
+ Object.freeze(value);
227
+ }
228
+ return value;
229
+ }
230
+ //# sourceMappingURL=feedback.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export type * from "./contracts.js";
2
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict } from "./contracts.js";
2
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError } from "./contracts.js";
3
3
  export { createAgent, createAgentSession } from "./agents.js";
4
+ export { createMemoryRunFeedbackStore, prepareRunFeedback, requireRunFeedbackOwnership, runFeedbackPageLimit, RunFeedbackError, } from "./feedback.js";
5
+ export type { MemoryRunFeedbackStoreOptions, PrepareRunFeedbackOptions, RunFeedbackLimits, RunFeedbackRun, RunFeedbackRunResolver, } from "./feedback.js";
4
6
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
5
7
  export type { MemoryCheckpointStoreOptions } from "./checkpoints.js";
6
8
  export { LEASE_CONFLICT_CODE, LeaseConflictError, createMemoryLeaseStore } from "./leases.js";
@@ -33,8 +35,8 @@ export type { ComposeSystemPromptOptions } from "./system-prompts.js";
33
35
  export type { ModelRegistry, ModelRegistryOptions } from "./models.js";
34
36
  export { definePrismManifest, parsePrismManifest } from "./manifests.js";
35
37
  export type { ManifestContributionDeclaration, ManifestContributionKind, ManifestResourceDeclaration, PrismManifest } from "./manifests.js";
36
- export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
37
- export type { AudioContent, DocumentContent, FileContent, MediaContentBlock, MediaContentBounds, MediaMimePolicy, ModelInputCapability, ResolvedMediaContent, ResolveMediaContentOptions, SsrfPolicy, } from "./content.js";
38
+ export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, resolveMediaContentBlocks, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
39
+ export type { AudioContent, DocumentContent, FileContent, MediaContentBlock, MediaContentBounds, MediaHostAddress, MediaHostnameResolver, MediaMimePolicy, MediaUrlRequest, MediaUrlRequester, ModelInputCapability, ResolvedMediaContent, ResolveMediaContentOptions, SsrfPolicy, } from "./content.js";
38
40
  export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
39
41
  export type { LoadBinaryResourceOptions } from "./resources.js";
40
42
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
@@ -65,5 +67,5 @@ export type { DispatchToolCallOptions, ToolArgumentValidationError, ToolArgument
65
67
  export type { DuplicateRegistrationOptions, DuplicateRegistrationPolicy } from "./registry-options.js";
66
68
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
67
69
  export declare const name = "prism";
68
- export declare const version = "0.0.4";
70
+ export declare const version = "0.0.5";
69
71
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict } from "./contracts.js";
1
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError } from "./contracts.js";
2
2
  export { createAgent, createAgentSession } from "./agents.js";
3
+ export { createMemoryRunFeedbackStore, prepareRunFeedback, requireRunFeedbackOwnership, runFeedbackPageLimit, RunFeedbackError, } from "./feedback.js";
3
4
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
4
5
  export { LEASE_CONFLICT_CODE, LeaseConflictError, createMemoryLeaseStore } from "./leases.js";
5
6
  export { createEventMultiplexer } from "./event-multiplexer.js";
@@ -19,7 +20,7 @@ export { createProviderTurnMetadata, readProviderHttpStatus } from "./observabil
19
20
  export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProviderRequestOptions } from "./provider-request-policy.js";
20
21
  export { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
21
22
  export { definePrismManifest, parsePrismManifest } from "./manifests.js";
22
- export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
23
+ export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, resolveMediaContentBlocks, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
23
24
  export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
24
25
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
25
26
  export { createMiddlewareRegistry } from "./middleware.js";
@@ -36,6 +37,6 @@ export { resolveInstructionInjectors, runInstructionInjectors } from "./instruct
36
37
  export { createToolRegistry, dispatchToolCall, filterTools, createToolParameterValidator } from "./tools.js";
37
38
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
38
39
  export const name = "prism";
39
- export const version = "0.0.4";
40
+ export const version = "0.0.5";
40
41
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
41
42
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import type { AudioContent, ContentBlock, DocumentContent, FileContent, JsonObject, ModelCapabilities, ModelConfig } from "../contracts.js";
1
+ import type { AudioContent, ContentBlock, DocumentContent, FileContent, JsonObject, Message, ModelCapabilities, ModelConfig } from "../contracts.js";
2
2
  import { type MediaContentBlock, type ModelInputCapability, type ResolveMediaContentOptions, type ResolvedMediaContent } from "../content.js";
3
3
  /** Default upload-cache entry cap per provider media session. */
4
4
  export declare const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
@@ -38,5 +38,7 @@ export declare function serializePdfDocumentWireBlock(options: {
38
38
  readonly title?: string;
39
39
  }): JsonObject;
40
40
  export declare function resolveProviderMediaBlock(block: AudioContent | FileContent | DocumentContent, options?: ResolveMediaContentOptions): Promise<ResolvedMediaContent>;
41
+ /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
42
+ export declare function resolveProviderMediaMessages(messages: readonly Message[], model: ModelConfig, options?: ResolveMediaContentOptions): Promise<ReadonlyMap<MediaContentBlock, ResolvedMediaContent>>;
41
43
  export declare function defaultProviderFilename(block: MediaContentBlock, fallback: string): string;
42
44
  export declare const DEFAULT_PROVIDER_MEDIA_ITEM_BYTES = 10000000;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlock, UnsupportedModalityError, } from "../content.js";
2
+ import { assertMessagesSupportModelCapabilities, contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlock, resolveMediaContentBlocks, UnsupportedModalityError, } from "../content.js";
3
3
  /** Default upload-cache entry cap per provider media session. */
4
4
  export const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
5
  /** Inline OpenAI file_data ceiling before preferring Files API upload. */
@@ -96,6 +96,16 @@ export function serializePdfDocumentWireBlock(options) {
96
96
  export async function resolveProviderMediaBlock(block, options = {}) {
97
97
  return resolveMediaContentBlock(block, options);
98
98
  }
99
+ /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
100
+ export async function resolveProviderMediaMessages(messages, model, options = {}) {
101
+ assertMessagesSupportModelCapabilities(model, messages);
102
+ const blocks = messages.flatMap((message) => message.content.filter(isMediaContentBlock));
103
+ const resolved = await resolveMediaContentBlocks(blocks, options);
104
+ return new Map(blocks.map((block, index) => [block, resolved[index]]));
105
+ }
106
+ function isMediaContentBlock(block) {
107
+ return contentBlockInputModality(block) !== undefined;
108
+ }
99
109
  export function defaultProviderFilename(block, fallback) {
100
110
  if ("name" in block && block.name)
101
111
  return block.name;
@@ -0,0 +1,6 @@
1
+ import type { RunFeedbackStore } from "../contracts.js";
2
+ export interface RunFeedbackConformanceFactory {
3
+ (): RunFeedbackStore | Promise<RunFeedbackStore>;
4
+ }
5
+ /** Shared minimum behavior for memory and production feedback stores. */
6
+ export declare function runFeedbackConformance(factory: RunFeedbackConformanceFactory): Promise<void>;
@@ -0,0 +1,37 @@
1
+ /** Shared minimum behavior for memory and production feedback stores. */
2
+ export async function runFeedbackConformance(factory) {
3
+ const store = await factory();
4
+ const owner = { tenantId: "feedback-tenant", userId: "feedback-user" };
5
+ await store.append({
6
+ id: "feedback-1",
7
+ runId: "feedback-run-a",
8
+ rating: 1,
9
+ comment: "useful",
10
+ tags: ["reviewed"],
11
+ evaluationIds: ["eval-1"],
12
+ createdAt: "2026-01-01T00:00:00.000Z",
13
+ ...owner,
14
+ });
15
+ await store.append({
16
+ id: "feedback-2",
17
+ runId: "feedback-run-a",
18
+ rating: 0,
19
+ createdAt: "2026-01-01T00:00:01.000Z",
20
+ ...owner,
21
+ });
22
+ const first = await store.query({ ...owner, runId: "feedback-run-a", limit: 1 });
23
+ if (first.items.length !== 1 || !first.nextCursor)
24
+ throw new Error("feedback first page is invalid");
25
+ const second = await store.query({ ...owner, runId: "feedback-run-a", cursor: first.nextCursor, limit: 1 });
26
+ if (second.items.length !== 1 || second.items[0]?.id === first.items[0]?.id)
27
+ throw new Error("feedback cursor did not advance");
28
+ const linked = await store.query({ ...owner, evaluationId: "eval-1" });
29
+ if (linked.items.length !== 1 || linked.items[0]?.id !== "feedback-1")
30
+ throw new Error("feedback evaluation filter failed");
31
+ if (await store.delete({ id: "feedback-1", tenantId: "feedback-tenant", userId: "other" })) {
32
+ throw new Error("cross-owner feedback deletion succeeded");
33
+ }
34
+ if (!await store.delete({ id: "feedback-1", ...owner }))
35
+ throw new Error("owned feedback deletion failed");
36
+ }
37
+ //# sourceMappingURL=feedback.js.map
@@ -1,8 +1,8 @@
1
1
  import type { PersistencePage, SessionEntry, SessionEntryQuery } from "../contracts.js";
2
2
  /** Current shared persistence schema version for production database adapters. */
3
- export declare const PERSISTENCE_SCHEMA_VERSION = 1;
4
- export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_tool_calls" | "prism_usage" | "prism_retention_policies" | "prism_migrations";
5
- export type PersistenceColumnType = "text" | "integer" | "boolean" | "json" | "timestamp";
3
+ export declare const PERSISTENCE_SCHEMA_VERSION = 3;
4
+ export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_migrations";
5
+ export type PersistenceColumnType = "text" | "integer" | "number" | "boolean" | "json" | "timestamp";
6
6
  export interface PersistenceColumnDefinition {
7
7
  readonly name: string;
8
8
  readonly type: PersistenceColumnType;
@@ -3,7 +3,7 @@
3
3
  // this module defines the shared table/index/pagination/migration expectations
4
4
  // adapter authors implement and test against before shipping dialect-specific DDL.
5
5
  /** Current shared persistence schema version for production database adapters. */
6
- export const PERSISTENCE_SCHEMA_VERSION = 1;
6
+ export const PERSISTENCE_SCHEMA_VERSION = 3;
7
7
  /** Guidance adapters must follow: values are bound parameters, never interpolated. */
8
8
  export const PARAMETERIZED_QUERY_GUIDANCE = "Bind every user-supplied value (session ids, idempotency keys, tenant ids, timestamps, JSON payloads) as a query parameter. Quote/validate schema and table identifiers only; never interpolate untrusted strings into SQL text.";
9
9
  const TENANT_COLUMNS = [
@@ -210,6 +210,9 @@ export function createPersistenceSchemaModel() {
210
210
  { name: "session_id", type: "text" },
211
211
  { name: "run_id", type: "text", nullable: true },
212
212
  { name: "entry_id", type: "text", nullable: true },
213
+ { name: "scope", type: "text" },
214
+ { name: "turn", type: "integer", nullable: true },
215
+ { name: "attempt", type: "integer", nullable: true },
213
216
  { name: "usage", type: "json" },
214
217
  { name: "recorded_at", type: "timestamp" },
215
218
  ...TENANT_COLUMNS,
@@ -217,6 +220,26 @@ export function createPersistenceSchemaModel() {
217
220
  ],
218
221
  foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
219
222
  },
223
+ {
224
+ name: "prism_run_feedback",
225
+ primaryKey: ["id"],
226
+ columns: [
227
+ { name: "id", type: "text" },
228
+ { name: "run_id", type: "text" },
229
+ { name: "session_id", type: "text" },
230
+ { name: "trace_id", type: "text", nullable: true },
231
+ { name: "rating", type: "number", nullable: true },
232
+ { name: "comment", type: "text", nullable: true },
233
+ { name: "tags", type: "json" },
234
+ { name: "scorer_ids", type: "json" },
235
+ { name: "evaluation_ids", type: "json" },
236
+ { name: "created_at", type: "timestamp" },
237
+ { name: "created_by", type: "text", nullable: true },
238
+ ...TENANT_COLUMNS,
239
+ { name: "metadata", type: "json", nullable: true },
240
+ ],
241
+ foreignKeys: [{ columns: ["run_id"], referencesTable: "prism_runs", referencesColumns: ["id"] }],
242
+ },
220
243
  {
221
244
  name: "prism_retention_policies",
222
245
  primaryKey: ["id"],
@@ -268,6 +291,10 @@ export function createPersistenceSchemaModel() {
268
291
  { name: "prism_tool_calls_run_started_idx", table: "prism_tool_calls", columns: ["run_id", "started_at"], purpose: "run tool-call listing" },
269
292
  { name: "prism_usage_run_recorded_idx", table: "prism_usage", columns: ["run_id", "recorded_at", "id"], purpose: "run usage pagination" },
270
293
  { name: "prism_usage_session_recorded_idx", table: "prism_usage", columns: ["session_id", "recorded_at"], purpose: "usage aggregation" },
294
+ { name: "prism_usage_session_scope_recorded_idx", table: "prism_usage", columns: ["session_id", "scope", "recorded_at"], purpose: "scope-safe usage aggregation" },
295
+ { name: "prism_run_feedback_owner_created_idx", table: "prism_run_feedback", columns: ["tenant_id", "account_id", "user_id", "created_at", "id"], purpose: "ownership-scoped feedback pagination" },
296
+ { name: "prism_run_feedback_run_created_idx", table: "prism_run_feedback", columns: ["run_id", "created_at", "id"], purpose: "run feedback lookup" },
297
+ { name: "prism_run_feedback_trace_created_idx", table: "prism_run_feedback", columns: ["trace_id", "created_at", "id"], purpose: "trace feedback lookup" },
271
298
  { name: "prism_agent_definitions_name_version_idx", table: "prism_agent_definitions", columns: ["name", "version"], purpose: "definition lookup" },
272
299
  { name: "prism_migrations_name_version_idx", table: "prism_migrations", columns: ["name", "version"], unique: true, purpose: "applied-migration uniqueness" },
273
300
  ],
@@ -280,6 +307,8 @@ export function createPersistenceMigrationContract() {
280
307
  appliedMigrationsTable: "prism_migrations",
281
308
  steps: [
282
309
  { version: 1, name: "001_init", description: "Create core session, branch, entry, idempotency, run, ledger, and migration tables." },
310
+ { version: 2, name: "002_usage_scope", description: "Distinguish provider-turn usage from aggregate run totals." },
311
+ { version: 3, name: "003_run_feedback", description: "Add immutable ownership-scoped run/trace feedback and evaluation links." },
283
312
  ],
284
313
  lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
285
314
  leastPrivilegeGuidance: "Run migrations with a DDL-capable role; use a separate least-privilege runtime role limited to INSERT/SELECT/UPDATE on adapter tables. Never grant migration credentials to the agent runtime.",
@@ -294,6 +323,7 @@ export function getPersistencePaginationCursors() {
294
323
  { table: "prism_agent_events", columns: ["session_id", "timestamp", "id"], supportsOrder: ["asc", "desc"], purpose: "session event stream" },
295
324
  { table: "prism_usage", columns: ["run_id", "recorded_at", "id"], supportsOrder: ["asc", "desc"], purpose: "run usage totals" },
296
325
  { table: "prism_tool_calls", columns: ["run_id", "started_at"], supportsOrder: ["asc", "desc"], purpose: "run tool-call listing" },
326
+ { table: "prism_run_feedback", columns: ["tenant_id", "account_id", "user_id", "created_at", "id"], supportsOrder: ["asc", "desc"], purpose: "owned feedback listing" },
297
327
  ];
298
328
  }
299
329
  /** Build a tenant-scoped unique key column list for adapter DDL. */
@@ -326,7 +356,7 @@ export function assertPersistenceSchemaModel(model) {
326
356
  throw new Error("Runs table must include tenant-scoped tenant_id");
327
357
  }
328
358
  const indexTables = new Set(model.indexes.map((index) => index.table));
329
- for (const requiredIndex of ["prism_session_append_idempotency", "prism_session_entries", "prism_agent_events", "prism_runs"]) {
359
+ for (const requiredIndex of ["prism_session_append_idempotency", "prism_session_entries", "prism_agent_events", "prism_runs", "prism_run_feedback"]) {
330
360
  if (!indexTables.has(requiredIndex)) {
331
361
  throw new Error(`Persistence schema missing indexes for ${requiredIndex}`);
332
362
  }
@@ -71,6 +71,9 @@ export async function assertRunLedgerConforms(fixture, options = {}) {
71
71
  id: "usage-1",
72
72
  sessionId,
73
73
  runId,
74
+ scope: "provider_turn",
75
+ turn: 1,
76
+ attempt: 2,
74
77
  usage: { inputTokens: 3, outputTokens: 5, totalTokens: 8 },
75
78
  recordedAt: "2026-01-01T00:00:01.000Z",
76
79
  ...scope,
@@ -114,8 +117,11 @@ export async function assertRunLedgerConforms(fixture, options = {}) {
114
117
  }
115
118
  if (fixture.readUsage) {
116
119
  const usageRows = await fixture.readUsage();
117
- if (!usageRows.some((row) => row.id === "usage-1")) {
120
+ const storedUsage = usageRows.find((row) => row.id === "usage-1");
121
+ if (!storedUsage)
118
122
  throw new Error("RunLedger must persist UsageRecord rows");
123
+ if (storedUsage.scope !== "provider_turn" || storedUsage.turn !== 1 || storedUsage.attempt !== 2) {
124
+ throw new Error("RunLedger must preserve UsageRecord scope, turn, and attempt");
119
125
  }
120
126
  }
121
127
  if (options.exerciseTenantIsolation && fixture.readRuns) {
package/docs/a2a.md ADDED
@@ -0,0 +1,73 @@
1
+ # A2A interoperability
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-supervisor` implements a bounded text-only subset of Agent2Agent (A2A) protocol 1.0: Agent Cards, JSON-RPC `SendMessage`, `SendStreamingMessage`, `GetExtendedAgentCard`, SSE task updates, ES256 JWS card signatures, and an explicit remote client.
6
+
7
+ ## When to use it
8
+
9
+ Use it to expose one explicitly selected Prism agent at an A2A endpoint or call a known remote A2A agent. Do not use it as endpoint discovery, a generic proxy, credential forwarding, or a replacement for local workflows.
10
+
11
+ ## Inputs / request
12
+
13
+ | API/field | Meaning |
14
+ | --- | --- |
15
+ | `createA2AAgentCard(card)` | Validates/freeze a JSONRPC protocol-1.0 HTTPS text card. |
16
+ | `signA2AAgentCard(card, { privateKey, keyId, expiresAt })` | Adds detached-payload ES256 JWS signature using WebCrypto. |
17
+ | `verifyA2AAgentCard(card, { publicKey, keyId?, now?, maxAgeMs? })` | Pins ES256/key/expiry and verifies canonical unsigned card. |
18
+ | `createA2AHandler({ card, exposure, authorize })` | Web-standard card/JSON-RPC/SSE `Request` to `Response` handler. |
19
+ | `createA2AClient({ endpoint, allowedOrigins })` | Explicit HTTPS remote client with optional card verifier/auth callback. |
20
+ | `A2ALimits` | Request 64 KiB, response 1 MiB, event 64 KiB, stream 10 MiB/10k events, concurrency 16, timeout 120s, card 64 KiB defaults; finite hard caps apply. |
21
+
22
+ ## Outputs / response / events
23
+
24
+ The handler serves `GET /.well-known/agent-card.json` and its configured POST endpoint. JSON-RPC returns `{ result: { task } }` or a bounded error. Streaming returns backpressure-driven SSE task envelopes. Client `send()` maps a terminal remote task to `AgentRunResult`; `stream()` yields validated/redacted text artifacts.
25
+
26
+ ## Request/response example
27
+
28
+ ```json
29
+ {"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"role":"user","messageId":"m1","parts":[{"text":"Check sources"}]}}}
30
+ ```
31
+
32
+ ## Implementation example
33
+
34
+ ```ts
35
+ import { createA2AClient, createA2AHandler, verifyA2AAgentCard } from "@arnilo/prism-supervisor";
36
+
37
+ const handler = createA2AHandler({
38
+ card,
39
+ exposure: { sessionFactory: ({ ownership }) => agent.createSession({ metadata: ownership }) },
40
+ authorize: ({ request }) => authenticate(request),
41
+ });
42
+
43
+ const client = createA2AClient({
44
+ endpoint: "https://agent.example/a2a/v1",
45
+ allowedOrigins: ["https://agent.example"],
46
+ authorize: () => ({ authorization: `Bearer ${resolveOwnedToken()}` }),
47
+ verifyCard: (remoteCard) => verifyA2AAgentCard(remoteCard, { publicKey, keyId: "agent-key" }),
48
+ });
49
+
50
+ const result = await client.send("Check sources");
51
+ ```
52
+
53
+ ## Extension and configuration notes
54
+
55
+ The package owns no listener or credential store. Mount the handler in a host server and resolve authentication/authorization on every request. Client auth executes only after body/card validation and serialization. Injectable `fetch` supports host transports/tests; redirects are disabled.
56
+
57
+ Only `text` parts are accepted. File/data parts, push notifications, task persistence/query/cancel, gRPC, HTTP+JSON binding, automatic JWK fetching, and endpoint discovery are intentionally absent.
58
+
59
+ ## Security and performance notes
60
+
61
+ - Endpoints and card URLs must be HTTPS and exactly origin-allow-listed before fetch; `redirect: "error"` prevents redirect SSRF.
62
+ - Treat every remote card, error, task, status, artifact, and SSE frame as untrusted. Shape/count/byte/time limits apply before mapping.
63
+ - Card verification pins `alg=ES256`, optional key ID, issue/expiry, optional maximum age, and canonical unsigned-card payload. Hosts provision trusted public keys; remote `jku` is never fetched automatically.
64
+ - Card discovery is public; extended-card and invoke methods call host authorization. Use TLS, rate limits, and replay controls at the host edge.
65
+ - Credentials remain in the client auth callback or server authorizer and never enter cards, messages, events, or metrics.
66
+ - Offline conformance is authoritative. Live endpoints are optional operator smoke tests.
67
+
68
+ ## Related APIs
69
+
70
+ - [Supervisor delegation](supervisors.md): local child boundary.
71
+ - [Web-standard server](server.md): non-A2A Prism routes.
72
+ - [Host security](host-security.md): authentication, SSRF, and untrusted-output policy.
73
+ - [Agent/session runtime](agent-session-runtime.md): mapped local execution/result.
@@ -8,7 +8,7 @@ Events are emitted by the runtime and by loops through `LoopContext.emit`, both
8
8
 
9
9
  ## When to use it
10
10
 
11
- Subscribe via `session.subscribe()` whenever a host needs to observe run progress: render streamed assistant text in a UI, react to tool execution, drive observability/telemetry, or audit artifact validation outcomes. Do not parse provider stream events directly for these — `AgentEvent` is the stable, normalized surface across providers and loops.
11
+ Subscribe via `session.stream()` for a single owned run, or `session.subscribe()` when a host needs a long-lived observer across runs: render streamed assistant text in a UI, react to tool execution, drive observability/telemetry, or audit artifact validation outcomes. Do not parse provider stream events directly for these — `AgentEvent` is the stable, normalized surface across providers and loops.
12
12
 
13
13
  Do not use `AgentEvent` for durable replay (use a `SessionStore`) or for cross-session coordination (the broadcaster is per-session and live-only).
14
14
 
@@ -58,7 +58,7 @@ Agent / turn / message events:
58
58
  | Variant | Fields |
59
59
  | --- | --- |
60
60
  | `agent_started` | `sessionId`, `runId` |
61
- | `agent_finished` | `sessionId`, `runId`, `usage?: Usage` |
61
+ | `agent_finished` | `sessionId`, `runId`, `usage?: Usage` (aggregate of all usage-bearing provider turns) |
62
62
  | `turn_started` / `turn_finished` | `sessionId`, `runId`, `turn: number` |
63
63
  | `message_started` / `message_finished` | `sessionId`, `runId`, `message: Message` |
64
64
  | `message_delta` | `sessionId`, `runId`, `content: ContentBlock` (`tool_call_delta` fragments may appear here for live UI streaming; stored messages use final `tool_call` blocks) |
@@ -173,12 +173,10 @@ const session = createAgent({
173
173
  provider: createMockProvider([providerTextDelta("ok"), providerDone()]),
174
174
  }).createSession();
175
175
 
176
- for await (const event of session.subscribe()) {
176
+ for await (const event of session.stream("draft", { loop: { strategy: "generate-validate-revise", validator, maxRevisions: 3 } })) {
177
177
  if (event.type === "artifact_finished") console.log("artifact ok", event.attempt);
178
178
  if (event.type === "artifact_failed") console.log("artifact exhausted", event.attempt, event.result.errors);
179
179
  }
180
-
181
- await session.run("draft", { loop: { strategy: "generate-validate-revise", validator, maxRevisions: 3 } });
182
180
  ```
183
181
 
184
182
  ## Extension and configuration notes
@@ -199,7 +197,7 @@ await session.run("draft", { loop: { strategy: "generate-validate-revise", valid
199
197
  - Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, tool results, or artifact validation payloads.
200
198
 
201
199
  ## Related APIs
202
- - [Agent/session runtime](agent-session-runtime.md): `session.subscribe()` and the live event broadcaster.
200
+ - [Agent/session runtime](agent-session-runtime.md): `session.stream()`, `session.subscribe()`, and the live event broadcaster.
203
201
  - [Agent loops](agent-loops.md): `singleShotLoop` and `generateValidateReviseLoop` emit the artifact events.
204
202
  - [Structured output](structured-output.md): `ArtifactValidation` shape threaded through parser/validator/repairer.
205
203
  - [Public contracts](public-contracts.md): full `AgentEvent` union and `ArtifactValidation` contract.
@@ -108,7 +108,7 @@ Host callback contracts (all generic over host `T`):
108
108
 
109
109
  ## Outputs / response / events
110
110
 
111
- `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` — the last provider usage, handed back to the runtime which emits `agent_finished` with it.
111
+ `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` as a fallback for custom loops. Core runtime independently accumulates every usage-bearing provider turn in O(turns), persists scoped turn/run rows, and emits `agent_finished` with the aggregate.
112
112
 
113
113
  Events during a loop run are the existing `AgentEvent`s (`turn_started`, `message_started`, `message_delta`, `message_finished`, `turn_finished`, tool-execution events when the loop dispatches tools, `error` on real failures). Both built-in loops emit `turn_started` before each provider turn, `message_finished` for every assistant draft, and `turn_finished` after the assistant draft is appended. First-turn input is appended to live history once, matching the already-persisted user message.
114
114