@arnilo/prism 0.0.10 → 0.0.12
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 +27 -0
- package/dist/agent-loops.d.ts +1 -0
- package/dist/agent-loops.js +37 -2
- package/dist/agent-run-lifecycle.d.ts +5 -1
- package/dist/agent-run-lifecycle.js +17 -1
- package/dist/agents.d.ts +3 -1
- package/dist/agents.js +202 -24
- package/dist/context-budget.d.ts +63 -0
- package/dist/context-budget.js +235 -0
- package/dist/contracts.d.ts +109 -0
- package/dist/contracts.js +77 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/input.d.ts +3 -0
- package/dist/input.js +71 -28
- package/dist/node/session-store-jsonl.js +4 -1
- package/dist/rpc.js +13 -2
- package/dist/session-stores.d.ts +7 -2
- package/dist/session-stores.js +174 -4
- package/dist/structured-output.d.ts +5 -1
- package/dist/structured-output.js +18 -0
- package/dist/testing/persistence-schema.d.ts +1 -1
- package/dist/testing/persistence-schema.js +8 -2
- package/dist/testing/session-store-conformance.d.ts +6 -0
- package/dist/testing/session-store-conformance.js +36 -1
- package/docs/a2a.md +1 -0
- package/docs/ag-ui.md +123 -0
- package/docs/agent-events.md +2 -1
- package/docs/agent-loops.md +8 -1
- package/docs/agent-session-runtime.md +10 -2
- package/docs/cli-rpc.md +2 -1
- package/docs/coding-agent-tools.md +70 -1
- package/docs/compaction-and-retry.md +2 -1
- package/docs/compaction-llm.md +20 -1
- package/docs/credential-storage.md +2 -1
- package/docs/credentials-and-redaction.md +8 -0
- package/docs/evaluations.md +1 -1
- package/docs/host-security.md +2 -0
- package/docs/index.md +20 -17
- package/docs/input-and-prompt-assembly.md +4 -1
- package/docs/migration.md +32 -0
- package/docs/node-jsonl-session-store.md +1 -1
- package/docs/performance.md +36 -0
- package/docs/postgres-persistence.md +3 -3
- package/docs/provider-packages.md +11 -1
- package/docs/providers/anthropic.md +93 -0
- package/docs/providers/google.md +88 -0
- package/docs/providers/openai.md +2 -2
- package/docs/public-contracts.md +6 -0
- package/docs/release-and-install.md +232 -65
- package/docs/review-coverage-2026-07-22-phase-6.md +209 -0
- package/docs/review-coverage-2026-07-22-phase-7.md +173 -0
- package/docs/runs-and-usage.md +1 -0
- package/docs/server.md +1 -0
- package/docs/session-store-conformance.md +2 -0
- package/docs/session-stores.md +40 -1
- package/docs/sqlite-persistence.md +3 -3
- package/docs/structured-output.md +7 -1
- package/docs/workflows.md +2 -0
- package/package.json +3 -2
package/dist/input.js
CHANGED
|
@@ -3,22 +3,12 @@ import { assertMessagesSupportModelCapabilities } from "./content.js";
|
|
|
3
3
|
import { loadTextResource } from "./resources.js";
|
|
4
4
|
import { composeSystemPrompt } from "./system-prompts.js";
|
|
5
5
|
import { runInstructionInjectors } from "./instruction-injection.js";
|
|
6
|
+
import { applyContextBudget, CONTEXT_BUDGET_REPORT_METADATA_KEY, } from "./context-budget.js";
|
|
6
7
|
export function createDefaultInputBuilder() {
|
|
7
8
|
return {
|
|
8
9
|
name: "default-input",
|
|
9
10
|
async build(input, context = {}) {
|
|
10
|
-
const groups =
|
|
11
|
-
instructions: [
|
|
12
|
-
...instructionMessages(context.systemInstructions, "System instruction"),
|
|
13
|
-
...instructionMessages(context.developerInstructions, "Developer instruction"),
|
|
14
|
-
...customInstructionMessages(context.instructions),
|
|
15
|
-
],
|
|
16
|
-
summaries: summaryMessages(context.summaries),
|
|
17
|
-
history: [...(context.history ?? [])],
|
|
18
|
-
input: inputMessages(input),
|
|
19
|
-
attachments: await attachmentMessages(context),
|
|
20
|
-
toolResults: toolResultMessages(context.toolResults),
|
|
21
|
-
};
|
|
11
|
+
const groups = await buildDefaultInputMessageGroups(input, context);
|
|
22
12
|
const messages = flattenInputGroups(groups, context.inputLayout ?? "legacy");
|
|
23
13
|
return context.middleware ? context.middleware.run("input_assembly", messages) : messages;
|
|
24
14
|
},
|
|
@@ -65,7 +55,6 @@ export function renderPromptTemplate(template, variables, options = {}) {
|
|
|
65
55
|
});
|
|
66
56
|
}
|
|
67
57
|
export async function assembleProviderInput(options) {
|
|
68
|
-
const inputBuilder = options.inputBuilder ?? createDefaultInputBuilder();
|
|
69
58
|
const baseContext = {
|
|
70
59
|
sessionId: options.sessionId,
|
|
71
60
|
runId: options.runId,
|
|
@@ -92,44 +81,98 @@ export async function assembleProviderInput(options) {
|
|
|
92
81
|
? composeSystemPrompt(injectorContribs.instructions, { base: options.systemInstructions })
|
|
93
82
|
: options.systemInstructions;
|
|
94
83
|
const buildContext = { ...options, ...baseContext, systemInstructions };
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
84
|
+
const layout = buildContext.inputLayout ?? "legacy";
|
|
85
|
+
let messages;
|
|
86
|
+
let context;
|
|
87
|
+
let skills = options.skills;
|
|
88
|
+
let tools = options.tools;
|
|
89
|
+
let budgetReport;
|
|
90
|
+
if (options.contextBudget) {
|
|
91
|
+
// ponytail: budget path always uses default groups so eviction kinds stay deterministic;
|
|
92
|
+
// custom inputBuilder still honored when contextBudget is absent.
|
|
93
|
+
const groups = await buildDefaultInputMessageGroups(options.input, buildContext);
|
|
94
|
+
context = await resolveContextProviders({
|
|
95
|
+
providers: options.contextProviders,
|
|
96
|
+
messages: flattenInputGroups(groups, layout),
|
|
97
|
+
injectedBlocks: injectorContribs.contextBlocks.length ? injectorContribs.contextBlocks : undefined,
|
|
98
|
+
middleware: options.middleware,
|
|
99
|
+
...baseContext,
|
|
100
|
+
});
|
|
101
|
+
const applied = applyContextBudget({
|
|
102
|
+
groups,
|
|
103
|
+
context,
|
|
104
|
+
skills: skills ?? [],
|
|
105
|
+
tools,
|
|
106
|
+
budget: options.contextBudget,
|
|
107
|
+
layout,
|
|
108
|
+
});
|
|
109
|
+
messages = flattenInputGroups(applied.groups, layout);
|
|
110
|
+
if (buildContext.middleware)
|
|
111
|
+
messages = await buildContext.middleware.run("input_assembly", messages);
|
|
112
|
+
context = applied.context;
|
|
113
|
+
skills = applied.skills;
|
|
114
|
+
tools = applied.tools;
|
|
115
|
+
if (options.contextBudget.reportOmissions)
|
|
116
|
+
budgetReport = applied.report;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const inputBuilder = options.inputBuilder ?? createDefaultInputBuilder();
|
|
120
|
+
messages = await inputBuilder.build(options.input, buildContext);
|
|
121
|
+
context = await resolveContextProviders({
|
|
122
|
+
providers: options.contextProviders,
|
|
123
|
+
messages,
|
|
124
|
+
injectedBlocks: injectorContribs.contextBlocks.length ? injectorContribs.contextBlocks : undefined,
|
|
125
|
+
middleware: options.middleware,
|
|
126
|
+
...baseContext,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
103
129
|
const promptBuilder = options.promptBuilder ?? createDefaultPromptBuilder();
|
|
104
130
|
const promptRequest = options.middleware
|
|
105
131
|
? await options.middleware.run("prompt_build", {
|
|
106
132
|
messages,
|
|
107
133
|
context,
|
|
108
|
-
skills
|
|
109
|
-
tools
|
|
134
|
+
skills,
|
|
135
|
+
tools,
|
|
110
136
|
metadata: options.metadata,
|
|
111
137
|
signal: options.signal,
|
|
112
138
|
})
|
|
113
139
|
: {
|
|
114
140
|
messages,
|
|
115
141
|
context,
|
|
116
|
-
skills
|
|
117
|
-
tools
|
|
142
|
+
skills,
|
|
143
|
+
tools,
|
|
118
144
|
metadata: options.metadata,
|
|
119
145
|
signal: options.signal,
|
|
120
146
|
};
|
|
121
|
-
const providerMessages = await promptBuilder.build({ ...promptRequest, tools
|
|
147
|
+
const providerMessages = await promptBuilder.build({ ...promptRequest, tools });
|
|
122
148
|
assertMessagesSupportModelCapabilities(options.model, providerMessages);
|
|
149
|
+
const metadata = budgetReport
|
|
150
|
+
? { ...options.metadata, [CONTEXT_BUDGET_REPORT_METADATA_KEY]: budgetReport }
|
|
151
|
+
: options.metadata;
|
|
123
152
|
return {
|
|
124
153
|
model: options.model,
|
|
125
154
|
messages: providerMessages,
|
|
126
|
-
tools
|
|
155
|
+
tools,
|
|
127
156
|
context,
|
|
128
157
|
options: options.providerOptions,
|
|
129
|
-
metadata
|
|
158
|
+
metadata,
|
|
130
159
|
signal: options.signal,
|
|
131
160
|
};
|
|
132
161
|
}
|
|
162
|
+
async function buildDefaultInputMessageGroups(input, context) {
|
|
163
|
+
return {
|
|
164
|
+
instructions: [
|
|
165
|
+
...instructionMessages(context.systemInstructions, "System instruction"),
|
|
166
|
+
...instructionMessages(context.developerInstructions, "Developer instruction"),
|
|
167
|
+
...customInstructionMessages(context.instructions),
|
|
168
|
+
],
|
|
169
|
+
summaries: summaryMessages(context.summaries),
|
|
170
|
+
history: [...(context.history ?? [])],
|
|
171
|
+
input: inputMessages(input),
|
|
172
|
+
attachments: await attachmentMessages(context),
|
|
173
|
+
toolResults: toolResultMessages(context.toolResults),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
133
176
|
function flattenInputGroups(groups, layout) {
|
|
134
177
|
switch (layout) {
|
|
135
178
|
case "cache_aware":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, appendFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError, isSessionEntryKind, SESSION_ENTRY_SCHEMA_VERSION } from "../contracts.js";
|
|
3
|
+
import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError, SessionSearchUnsupportedError, isSessionEntryKind, SESSION_ENTRY_SCHEMA_VERSION, } from "../contracts.js";
|
|
4
4
|
import { isNodeErrorCode } from "./config.js";
|
|
5
5
|
export function createJsonlSessionStore(pathOrOptions) {
|
|
6
6
|
const options = typeof pathOrOptions === "string" ? { path: pathOrOptions, createDirectory: true } : pathOrOptions;
|
|
@@ -46,6 +46,9 @@ export function createJsonlSessionStore(pathOrOptions) {
|
|
|
46
46
|
async get(id) {
|
|
47
47
|
return findEntry(path, id);
|
|
48
48
|
},
|
|
49
|
+
async searchSessions() {
|
|
50
|
+
throw new SessionSearchUnsupportedError("JSONL session store does not support searchSessions");
|
|
51
|
+
},
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
/** Read a JSONL session file and return both valid entries and per-line parse errors. */
|
package/dist/rpc.js
CHANGED
|
@@ -82,8 +82,19 @@ async function handleRequest(request, state, stdout, activeRuns) {
|
|
|
82
82
|
activeRuns.set(session.id, { requestId: request.id, promise });
|
|
83
83
|
break;
|
|
84
84
|
}
|
|
85
|
-
case "steer":
|
|
86
|
-
|
|
85
|
+
case "steer": {
|
|
86
|
+
const input = stringParam(request.params, "input") ?? stringParam(request.params, "prompt");
|
|
87
|
+
if (!input)
|
|
88
|
+
throw new Error("steer requires params.input");
|
|
89
|
+
const softInterrupt = request.params?.softInterrupt === true;
|
|
90
|
+
state.current.steer(input, softInterrupt ? { softInterrupt: true } : undefined);
|
|
91
|
+
write(stdout, {
|
|
92
|
+
id: request.id,
|
|
93
|
+
ok: true,
|
|
94
|
+
result: { sessionId: state.current.id },
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
87
98
|
case "abort":
|
|
88
99
|
state.current.abort(stringParam(request.params, "reason"));
|
|
89
100
|
write(stdout, { id: request.id, ok: true, result: { sessionId: state.current.id } });
|
package/dist/session-stores.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Message, type SessionBranchRead, type BranchReader, type SessionEntry, 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;
|
|
@@ -24,4 +24,9 @@ export declare function getSessionBranchEntries(entries: readonly SessionEntry[]
|
|
|
24
24
|
export declare function listSessionBranches(entries: readonly SessionEntry[]): readonly SessionBranch[];
|
|
25
25
|
export declare function rebuildSessionContext(reader: BranchReader, query: SessionBranchRead): Promise<SessionContextSnapshot>;
|
|
26
26
|
export declare function rebuildSessionContext(entries: readonly SessionEntry[], options?: SessionBranchOptions): SessionContextSnapshot;
|
|
27
|
-
export
|
|
27
|
+
export type MemorySessionSearchMode = "linear" | "unsupported";
|
|
28
|
+
export interface CreateMemorySessionStoreOptions {
|
|
29
|
+
/** Default `"linear"`: capped in-process scan. `"unsupported"`: typed throw. */
|
|
30
|
+
readonly sessionSearchMode?: MemorySessionSearchMode;
|
|
31
|
+
}
|
|
32
|
+
export declare function createMemorySessionStore(initialEntries?: readonly SessionEntry[], options?: CreateMemorySessionStoreOptions): SessionStore;
|
package/dist/session-stores.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError } from "./contracts.js";
|
|
1
|
+
import { DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, SESSION_APPEND_CONFLICT_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, resolveSessionSearchQuery, } from "./contracts.js";
|
|
2
2
|
import { createId } from "./ids.js";
|
|
3
3
|
export function createSessionEntry(options) {
|
|
4
4
|
const { createId, now, ...entry } = options;
|
|
@@ -93,16 +93,17 @@ function rebuildSessionContextCore(entries, options = {}) {
|
|
|
93
93
|
}
|
|
94
94
|
return { leafId: branch.at(-1)?.id, entries: branch, messages, summaries };
|
|
95
95
|
}
|
|
96
|
-
export function createMemorySessionStore(initialEntries = []) {
|
|
96
|
+
export function createMemorySessionStore(initialEntries = [], options = {}) {
|
|
97
97
|
const byId = new Map();
|
|
98
98
|
const bySession = new Map();
|
|
99
99
|
const leafBySession = new Map();
|
|
100
100
|
const idempotencySeen = new Set();
|
|
101
|
+
const mode = options.sessionSearchMode ?? "linear";
|
|
101
102
|
for (const entry of initialEntries)
|
|
102
103
|
add(entry);
|
|
103
104
|
return {
|
|
104
|
-
async append(entry,
|
|
105
|
-
add(entry,
|
|
105
|
+
async append(entry, appendOptions) {
|
|
106
|
+
add(entry, appendOptions);
|
|
106
107
|
},
|
|
107
108
|
async list(sessionId) {
|
|
108
109
|
return (bySession.get(sessionId) ?? []).map(cloneEntry);
|
|
@@ -111,6 +112,11 @@ export function createMemorySessionStore(initialEntries = []) {
|
|
|
111
112
|
const entry = byId.get(id);
|
|
112
113
|
return entry ? cloneEntry(entry) : undefined;
|
|
113
114
|
},
|
|
115
|
+
async searchSessions(query) {
|
|
116
|
+
if (mode === "unsupported")
|
|
117
|
+
throw new SessionSearchUnsupportedError();
|
|
118
|
+
return searchMemorySessionsLinear(bySession, leafBySession, query);
|
|
119
|
+
},
|
|
114
120
|
};
|
|
115
121
|
function add(entry, options) {
|
|
116
122
|
// ponytail: idempotency dedup keyed on (session, key, expectedParentId) so a
|
|
@@ -146,6 +152,170 @@ export function createMemorySessionStore(initialEntries = []) {
|
|
|
146
152
|
leafBySession.set(entry.sessionId, entry.id);
|
|
147
153
|
}
|
|
148
154
|
}
|
|
155
|
+
function searchMemorySessionsLinear(bySession, leafBySession, query) {
|
|
156
|
+
const q = resolveSessionSearchQuery(query);
|
|
157
|
+
q.signal?.throwIfAborted();
|
|
158
|
+
let sessionsScanned = 0;
|
|
159
|
+
let entriesScanned = 0;
|
|
160
|
+
let bytesScanned = 0;
|
|
161
|
+
const matches = [];
|
|
162
|
+
for (const [sessionId, entries] of bySession) {
|
|
163
|
+
if (sessionsScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS)
|
|
164
|
+
break;
|
|
165
|
+
if (entriesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES)
|
|
166
|
+
break;
|
|
167
|
+
if (bytesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES)
|
|
168
|
+
break;
|
|
169
|
+
q.signal?.throwIfAborted();
|
|
170
|
+
sessionsScanned += 1;
|
|
171
|
+
let updatedAt = "";
|
|
172
|
+
let label;
|
|
173
|
+
let summary;
|
|
174
|
+
let workspaceRoot;
|
|
175
|
+
let tenantId;
|
|
176
|
+
let accountId;
|
|
177
|
+
let userId;
|
|
178
|
+
let matchedLabel = false;
|
|
179
|
+
let matchedSummary = false;
|
|
180
|
+
let matchedQuery = false;
|
|
181
|
+
let matchedProvider = false;
|
|
182
|
+
let matchedModel = false;
|
|
183
|
+
let snippetSource;
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entriesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES)
|
|
186
|
+
break;
|
|
187
|
+
if (bytesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES)
|
|
188
|
+
break;
|
|
189
|
+
entriesScanned += 1;
|
|
190
|
+
const text = entrySearchText(entry);
|
|
191
|
+
bytesScanned += utf8Bytes(text) + utf8Bytes(entry.label) + utf8Bytes(entry.summary);
|
|
192
|
+
if (entry.timestamp > updatedAt)
|
|
193
|
+
updatedAt = entry.timestamp;
|
|
194
|
+
if (entry.label)
|
|
195
|
+
label = entry.label;
|
|
196
|
+
if (entry.summary)
|
|
197
|
+
summary = entry.summary;
|
|
198
|
+
const meta = entry.metadata;
|
|
199
|
+
if (meta) {
|
|
200
|
+
if (typeof meta[SESSION_SEARCH_WORKSPACE_METADATA_KEY] === "string") {
|
|
201
|
+
workspaceRoot = meta[SESSION_SEARCH_WORKSPACE_METADATA_KEY];
|
|
202
|
+
}
|
|
203
|
+
if (typeof meta.tenantId === "string")
|
|
204
|
+
tenantId = meta.tenantId;
|
|
205
|
+
if (typeof meta.accountId === "string")
|
|
206
|
+
accountId = meta.accountId;
|
|
207
|
+
if (typeof meta.userId === "string")
|
|
208
|
+
userId = meta.userId;
|
|
209
|
+
}
|
|
210
|
+
if (q.label && entry.label?.includes(q.label))
|
|
211
|
+
matchedLabel = true;
|
|
212
|
+
if (q.summary && entry.summary?.includes(q.summary))
|
|
213
|
+
matchedSummary = true;
|
|
214
|
+
if (q.query) {
|
|
215
|
+
const hay = `${entry.label ?? ""}\n${entry.summary ?? ""}\n${text}`;
|
|
216
|
+
if (hay.includes(q.query)) {
|
|
217
|
+
matchedQuery = true;
|
|
218
|
+
snippetSource ??= entry.label ?? entry.summary ?? text;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (q.provider && (entry.model?.provider === q.provider || metaProvider(entry) === q.provider))
|
|
222
|
+
matchedProvider = true;
|
|
223
|
+
if (q.model && entry.model?.model === q.model)
|
|
224
|
+
matchedModel = true;
|
|
225
|
+
}
|
|
226
|
+
if (q.workspaceRoot && workspaceRoot !== q.workspaceRoot)
|
|
227
|
+
continue;
|
|
228
|
+
if (q.tenantId && tenantId !== q.tenantId)
|
|
229
|
+
continue;
|
|
230
|
+
if (q.accountId && accountId !== q.accountId)
|
|
231
|
+
continue;
|
|
232
|
+
if (q.userId && userId !== q.userId)
|
|
233
|
+
continue;
|
|
234
|
+
if (q.label && !matchedLabel)
|
|
235
|
+
continue;
|
|
236
|
+
if (q.summary && !matchedSummary)
|
|
237
|
+
continue;
|
|
238
|
+
if (q.query && !matchedQuery)
|
|
239
|
+
continue;
|
|
240
|
+
if (q.provider && !matchedProvider)
|
|
241
|
+
continue;
|
|
242
|
+
if (q.model && !matchedModel)
|
|
243
|
+
continue;
|
|
244
|
+
if (q.fromUpdatedAt && updatedAt < q.fromUpdatedAt)
|
|
245
|
+
continue;
|
|
246
|
+
if (q.toUpdatedAt && updatedAt > q.toUpdatedAt)
|
|
247
|
+
continue;
|
|
248
|
+
matches.push({
|
|
249
|
+
sessionId,
|
|
250
|
+
leafId: leafBySession.get(sessionId),
|
|
251
|
+
updatedAt: updatedAt || undefined,
|
|
252
|
+
label,
|
|
253
|
+
summary,
|
|
254
|
+
snippet: clipSnippet(snippetSource ?? label ?? summary),
|
|
255
|
+
metadata: workspaceRoot !== undefined ? { [SESSION_SEARCH_WORKSPACE_METADATA_KEY]: workspaceRoot } : undefined,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
matches.sort((a, b) => compareSearchHit(a, b, q.order));
|
|
259
|
+
const afterCursor = q.cursor ? decodeSearchCursor(q.cursor) : undefined;
|
|
260
|
+
const filtered = afterCursor
|
|
261
|
+
? matches.filter((hit) => isAfterSearchCursor(hit, afterCursor, q.order))
|
|
262
|
+
: matches;
|
|
263
|
+
const page = filtered.slice(0, q.limit);
|
|
264
|
+
const last = page.at(-1);
|
|
265
|
+
return {
|
|
266
|
+
items: page,
|
|
267
|
+
nextCursor: filtered.length > q.limit && last?.updatedAt
|
|
268
|
+
? encodeSearchCursor(last.updatedAt, last.sessionId)
|
|
269
|
+
: undefined,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function entrySearchText(entry) {
|
|
273
|
+
if (!entry.message?.content)
|
|
274
|
+
return "";
|
|
275
|
+
const parts = [];
|
|
276
|
+
for (const block of entry.message.content) {
|
|
277
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
278
|
+
parts.push(block.text);
|
|
279
|
+
}
|
|
280
|
+
return parts.join("\n");
|
|
281
|
+
}
|
|
282
|
+
function metaProvider(entry) {
|
|
283
|
+
const value = entry.metadata?.provider;
|
|
284
|
+
return typeof value === "string" ? value : undefined;
|
|
285
|
+
}
|
|
286
|
+
function utf8Bytes(value) {
|
|
287
|
+
return value ? new TextEncoder().encode(value).byteLength : 0;
|
|
288
|
+
}
|
|
289
|
+
function clipSnippet(value) {
|
|
290
|
+
if (!value)
|
|
291
|
+
return undefined;
|
|
292
|
+
const encoded = new TextEncoder().encode(value);
|
|
293
|
+
if (encoded.byteLength <= DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES)
|
|
294
|
+
return value;
|
|
295
|
+
return new TextDecoder().decode(encoded.slice(0, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES));
|
|
296
|
+
}
|
|
297
|
+
function encodeSearchCursor(updatedAt, sessionId) {
|
|
298
|
+
return `${updatedAt}\t${sessionId}`;
|
|
299
|
+
}
|
|
300
|
+
function decodeSearchCursor(cursor) {
|
|
301
|
+
const tab = cursor.indexOf("\t");
|
|
302
|
+
if (tab <= 0 || tab === cursor.length - 1)
|
|
303
|
+
throw new TypeError("SessionSearchQuery.cursor is invalid");
|
|
304
|
+
return { updatedAt: cursor.slice(0, tab), sessionId: cursor.slice(tab + 1) };
|
|
305
|
+
}
|
|
306
|
+
function compareSearchHit(a, b, order) {
|
|
307
|
+
const aAt = a.updatedAt ?? "";
|
|
308
|
+
const bAt = b.updatedAt ?? "";
|
|
309
|
+
const cmp = aAt < bAt ? -1 : aAt > bAt ? 1 : a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0;
|
|
310
|
+
return order === "asc" ? cmp : -cmp;
|
|
311
|
+
}
|
|
312
|
+
function isAfterSearchCursor(hit, cursor, order) {
|
|
313
|
+
const at = hit.updatedAt ?? "";
|
|
314
|
+
if (order === "asc") {
|
|
315
|
+
return at > cursor.updatedAt || (at === cursor.updatedAt && hit.sessionId > cursor.sessionId);
|
|
316
|
+
}
|
|
317
|
+
return at < cursor.updatedAt || (at === cursor.updatedAt && hit.sessionId < cursor.sessionId);
|
|
318
|
+
}
|
|
149
319
|
function cloneEntry(entry) {
|
|
150
320
|
return structuredClone(entry);
|
|
151
321
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig, ModelCapabilities, ModelConfig, ProviderRequestOptions, RunOptions, StructuredOutputOptions } from "./contracts.js";
|
|
1
|
+
import type { AgentConfig, ModelCapabilities, ModelConfig, ProviderRequest, ProviderRequestOptions, RunOptions, StructuredOutputOptions } from "./contracts.js";
|
|
2
2
|
export declare const DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES = 65536;
|
|
3
3
|
export declare const DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH = 128;
|
|
4
4
|
export declare class StructuredOutputError extends Error {
|
|
@@ -9,3 +9,7 @@ export declare function modelSupportsStructuredOutput(capabilities?: ModelCapabi
|
|
|
9
9
|
export declare function validateStructuredOutputOptions(options: StructuredOutputOptions): StructuredOutputOptions;
|
|
10
10
|
export declare function assertStructuredOutputRequestSupported(model: ModelConfig, options?: ProviderRequestOptions): void;
|
|
11
11
|
export declare function resolveRunProviderOptions(runOptions: Pick<RunOptions, "providerOptions" | "loop">, config: Pick<AgentConfig, "providerOptions" | "loop">): ProviderRequestOptions | undefined;
|
|
12
|
+
/** Strip native schema so a tool-eligible turn can call tools freely. */
|
|
13
|
+
export declare function withoutStructuredOutput(request: ProviderRequest): ProviderRequest;
|
|
14
|
+
/** Artifact/revision turn: keep/restore schema and withdraw tools. */
|
|
15
|
+
export declare function artifactStructuredOutputRequest(request: ProviderRequest, schema?: StructuredOutputOptions): ProviderRequest;
|
|
@@ -56,4 +56,22 @@ export function resolveRunProviderOptions(runOptions, config) {
|
|
|
56
56
|
structuredOutput: validateStructuredOutputOptions(loop.structuredOutput),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
/** Strip native schema so a tool-eligible turn can call tools freely. */
|
|
60
|
+
export function withoutStructuredOutput(request) {
|
|
61
|
+
if (!request.options?.structuredOutput)
|
|
62
|
+
return request;
|
|
63
|
+
const { structuredOutput: _drop, ...options } = request.options;
|
|
64
|
+
return { ...request, options: Object.keys(options).length > 0 ? options : undefined };
|
|
65
|
+
}
|
|
66
|
+
/** Artifact/revision turn: keep/restore schema and withdraw tools. */
|
|
67
|
+
export function artifactStructuredOutputRequest(request, schema) {
|
|
68
|
+
const structuredOutput = schema ?? request.options?.structuredOutput;
|
|
69
|
+
if (!structuredOutput)
|
|
70
|
+
return { ...request, tools: undefined };
|
|
71
|
+
return {
|
|
72
|
+
...request,
|
|
73
|
+
tools: undefined,
|
|
74
|
+
options: { ...request.options, structuredOutput },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
59
77
|
//# sourceMappingURL=structured-output.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
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 =
|
|
3
|
+
export declare const PERSISTENCE_SCHEMA_VERSION = 4;
|
|
4
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
5
|
export type PersistenceColumnType = "text" | "integer" | "number" | "boolean" | "json" | "timestamp";
|
|
6
6
|
export interface PersistenceColumnDefinition {
|
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
// this module defines the shared table/index/pagination/migration expectations
|
|
5
5
|
// adapter authors implement and test against before shipping dialect-specific DDL.
|
|
6
6
|
/** Current shared persistence schema version for production database adapters. */
|
|
7
|
-
export const PERSISTENCE_SCHEMA_VERSION =
|
|
7
|
+
export const PERSISTENCE_SCHEMA_VERSION = 4;
|
|
8
8
|
/** Guidance adapters must follow: values are bound parameters, never interpolated. */
|
|
9
9
|
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.";
|
|
10
10
|
const TENANT_COLUMNS = [
|
|
@@ -315,7 +315,12 @@ function migrationStep(version, name, description) {
|
|
|
315
315
|
}
|
|
316
316
|
: version === 2
|
|
317
317
|
? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
|
|
318
|
-
:
|
|
318
|
+
: version === 3
|
|
319
|
+
? { tables: ["prism_run_feedback"], indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name) }
|
|
320
|
+
: version === 4
|
|
321
|
+
// Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
|
|
322
|
+
? { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
|
|
323
|
+
: (() => { throw new Error(`Unknown migration version ${version}`); })();
|
|
319
324
|
return {
|
|
320
325
|
version,
|
|
321
326
|
name,
|
|
@@ -332,6 +337,7 @@ export function createPersistenceMigrationContract() {
|
|
|
332
337
|
migrationStep(1, "001_init", "Create core session, branch, entry, idempotency, run, ledger, and migration tables."),
|
|
333
338
|
migrationStep(2, "002_usage_scope", "Distinguish provider-turn usage from aggregate run totals."),
|
|
334
339
|
migrationStep(3, "003_run_feedback", "Add immutable ownership-scoped run/trace feedback and evaluation links."),
|
|
340
|
+
migrationStep(4, "004_session_search", "Add bounded session search indexes and adapter-local FTS objects."),
|
|
335
341
|
],
|
|
336
342
|
lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
|
|
337
343
|
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.",
|
|
@@ -10,6 +10,12 @@ export interface SessionStoreConformanceOptions {
|
|
|
10
10
|
* when the store does not implement `readBranchPath`.
|
|
11
11
|
*/
|
|
12
12
|
readonly exerciseReadBranchPath?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* When true, exercises optional `searchSessions` (empty page, limit cap,
|
|
15
|
+
* invalid limit/query rejection via `resolveSessionSearchQuery` semantics).
|
|
16
|
+
* Skipped when the store does not implement `searchSessions`.
|
|
17
|
+
*/
|
|
18
|
+
readonly exerciseSearchSessions?: boolean;
|
|
13
19
|
/** When true, appends concurrent children of the same parent (fork allowed). */
|
|
14
20
|
readonly exerciseConcurrentParentAppend?: boolean;
|
|
15
21
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// stores already satisfy. Mirrors the assertion shape already repeated across
|
|
6
6
|
// src/__tests__/session-stores.test.ts and node-session-store-jsonl.test.ts so
|
|
7
7
|
// adapter authors do not re-derive them. Throws plain Error; no test runner.
|
|
8
|
-
import { isSessionAppendConflict } from "../contracts.js";
|
|
8
|
+
import { HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, isSessionAppendConflict, resolveSessionSearchQuery, } from "../contracts.js";
|
|
9
9
|
/**
|
|
10
10
|
* Assert that a `SessionStore` implementation satisfies the core adapter
|
|
11
11
|
* contract: round-trip append/list, duplicate-entry-id rejection,
|
|
@@ -63,6 +63,9 @@ export async function assertSessionStoreConforms(store, options = {}) {
|
|
|
63
63
|
throw new Error(`readBranchPath must return the ancestor chain root→leaf in order; got ${JSON.stringify(ids)}`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
if (options.exerciseSearchSessions && typeof store.searchSessions === "function") {
|
|
67
|
+
await assertSessionStoreSearchSessions(store);
|
|
68
|
+
}
|
|
66
69
|
await assertSessionStoreBranchIsolation(store, options);
|
|
67
70
|
if (options.exerciseConcurrentParentAppend) {
|
|
68
71
|
await assertConcurrentParentAppendAllowed(store, sessionId, now, make);
|
|
@@ -104,6 +107,38 @@ export async function runSessionStoreConformance(factory, options = {}) {
|
|
|
104
107
|
label: "dup",
|
|
105
108
|
}, { idempotencyKey: "reopen-idem", expectedParentId: parent?.id }), (error) => isSessionAppendConflict(error) && error.conflict.idempotencyDuplicate === true, "Restarted store must still deduplicate an exact idempotency retry");
|
|
106
109
|
}
|
|
110
|
+
async function assertSessionStoreSearchSessions(store) {
|
|
111
|
+
const search = store.searchSessions;
|
|
112
|
+
await reject(() => search({ limit: 0 }), (error) => error instanceof TypeError, "searchSessions must reject non-positive limit");
|
|
113
|
+
await reject(() => search({ limit: Number.NaN }), (error) => error instanceof TypeError, "searchSessions must reject NaN limit");
|
|
114
|
+
await reject(() => search({ limit: HARD_MAX_SESSION_SEARCH_LIMIT + 1 }), (error) => error instanceof TypeError, "searchSessions must reject oversize limit");
|
|
115
|
+
await reject(() => search({ query: "x".repeat(HARD_MAX_SESSION_SEARCH_QUERY_BYTES + 1) }), (error) => error instanceof TypeError, "searchSessions must reject oversize query string");
|
|
116
|
+
const empty = await search(resolveSessionSearchQuery({
|
|
117
|
+
workspaceRoot: "__prism_conformance_empty__",
|
|
118
|
+
limit: 5,
|
|
119
|
+
}));
|
|
120
|
+
if (!Array.isArray(empty.items)) {
|
|
121
|
+
throw new Error("searchSessions must return a PersistencePage with an items array");
|
|
122
|
+
}
|
|
123
|
+
const resolved = resolveSessionSearchQuery({ limit: 1 });
|
|
124
|
+
const page = await search(resolved);
|
|
125
|
+
if (page.items.length > resolved.limit) {
|
|
126
|
+
throw new Error(`searchSessions must honor limit; got ${page.items.length} items for limit ${resolved.limit}`);
|
|
127
|
+
}
|
|
128
|
+
for (const hit of page.items) {
|
|
129
|
+
if (typeof hit.sessionId !== "string" || !hit.sessionId) {
|
|
130
|
+
throw new Error("searchSessions hits must include a non-empty sessionId");
|
|
131
|
+
}
|
|
132
|
+
if ("credential" in hit || "apiKey" in hit || "password" in hit) {
|
|
133
|
+
throw new Error("searchSessions hits must not include credential fields");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const unscoped = await search({ limit: 100 });
|
|
137
|
+
const owned = await search({ tenantId: "__missing_tenant__", limit: 100 });
|
|
138
|
+
if (owned.items.length > unscoped.items.length) {
|
|
139
|
+
throw new Error("searchSessions ownership filter must not return more hits than an unscoped search");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
107
142
|
async function assertSessionStoreBranchIsolation(store, options) {
|
|
108
143
|
const sessionId = options.sessionId ?? "conformance";
|
|
109
144
|
const otherSessionId = options.otherSessionId ?? `${sessionId}-other`;
|
package/docs/a2a.md
CHANGED
|
@@ -92,3 +92,4 @@ Defaults/hard caps include: request 64 KiB/1 MiB; response 1/8 MiB; event 64 KiB
|
|
|
92
92
|
- [Agent/session runtime](agent-session-runtime.md)
|
|
93
93
|
- [Workflows](workflows.md)
|
|
94
94
|
- [Host security](host-security.md)
|
|
95
|
+
- [Frontend interoperability (AG-UI and ACP)](ag-ui.md): browser/editor protocol adapters over a Prism session; not an A2A card, task lifecycle, or remote-agent transport.
|