@vercel/agent-runs 1.0.0-rc.1 → 1.0.0-rc.3
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 +10 -5
- package/NOTICE +24 -0
- package/README.md +2 -2
- package/dist/src/adapters/serialization.d.ts.map +1 -1
- package/dist/src/adapters/serialization.js +34 -8
- package/dist/src/adapters/serialization.js.map +1 -1
- package/dist/src/core/content-policy.d.ts +8 -0
- package/dist/src/core/content-policy.d.ts.map +1 -1
- package/dist/src/core/content-policy.js +14 -0
- package/dist/src/core/content-policy.js.map +1 -1
- package/dist/src/core/scopes.d.ts.map +1 -1
- package/dist/src/core/scopes.js +0 -8
- package/dist/src/core/scopes.js.map +1 -1
- package/dist/src/core/snapshot.d.ts.map +1 -1
- package/dist/src/core/snapshot.js +126 -10
- package/dist/src/core/snapshot.js.map +1 -1
- package/dist/src/core/span-kinds.d.ts.map +1 -1
- package/dist/src/core/span-kinds.js +7 -3
- package/dist/src/core/span-kinds.js.map +1 -1
- package/dist/src/core/types.d.ts +15 -6
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/operations.d.ts +12 -4
- package/dist/src/operations.d.ts.map +1 -1
- package/dist/src/operations.js +6 -8
- package/dist/src/operations.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/serialization.ts +31 -7
- package/src/core/content-policy.ts +17 -0
- package/src/core/scopes.ts +0 -8
- package/src/core/snapshot.ts +135 -11
- package/src/core/span-kinds.ts +7 -3
- package/src/core/types.ts +15 -9
- package/src/index.ts +1 -0
- package/src/operations.ts +23 -17
package/src/core/snapshot.ts
CHANGED
|
@@ -1,10 +1,121 @@
|
|
|
1
1
|
import type { ScopeRecord } from "./types.js";
|
|
2
2
|
import type { ContentSerializer } from "./types.js";
|
|
3
3
|
import { capturedScopeData } from "./span-kinds.js";
|
|
4
|
-
const SERIALIZED_BYTES =
|
|
5
|
-
const SNAPSHOT_BYTES =
|
|
4
|
+
const SERIALIZED_BYTES = 256 * 1024;
|
|
5
|
+
const SNAPSHOT_BYTES = 512 * 1024;
|
|
6
6
|
const UNFINISHED_CHILDREN = 10000;
|
|
7
|
-
import { withoutDeclinedContent } from "./content-policy.js";
|
|
7
|
+
import { preservingCapturePolicy, withoutDeclinedContent } from "./content-policy.js";
|
|
8
|
+
|
|
9
|
+
const TRUNCATION_MARKER = "… [truncated]";
|
|
10
|
+
const TURN_CONTENT_ATTRIBUTES = ["gen_ai.input.messages", "gen_ai.output.messages"] as const;
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
function byteLength(value: string): number {
|
|
14
|
+
return encoder.encode(value).length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Text of a serialized GenAI message's text parts, the truncatable content. */
|
|
18
|
+
function messageText(parts: unknown): string {
|
|
19
|
+
if (!Array.isArray(parts)) return "";
|
|
20
|
+
let text = "";
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (typeof part !== "object" || part === null) continue;
|
|
23
|
+
const record = part as Record<string, unknown>;
|
|
24
|
+
if (record.type !== "text" || typeof record.content !== "string") continue;
|
|
25
|
+
text += (text.length === 0 ? "" : "\n") + record.content;
|
|
26
|
+
}
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shrinks a serialized GenAI message array to `budgetBytes` while keeping it
|
|
32
|
+
* valid JSON: the newest message's text is truncated with the library's
|
|
33
|
+
* truncation marker, mirroring how the serializer caps oversized prompts.
|
|
34
|
+
* The cut is binary searched, so an attribute far over the budget costs a
|
|
35
|
+
* logarithmic number of encodes instead of re-encoding a near-full candidate
|
|
36
|
+
* at every step.
|
|
37
|
+
*/
|
|
38
|
+
function fitTurnContent(input: string, budgetBytes: number): string | undefined {
|
|
39
|
+
if (budgetBytes <= 0) return undefined;
|
|
40
|
+
if (byteLength(input) <= budgetBytes) return input;
|
|
41
|
+
let messages: unknown;
|
|
42
|
+
try {
|
|
43
|
+
messages = JSON.parse(input);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(messages) || messages.length === 0) return undefined;
|
|
48
|
+
const last = messages.at(-1);
|
|
49
|
+
if (typeof last !== "object" || last === null) return undefined;
|
|
50
|
+
const message = last as Record<string, unknown>;
|
|
51
|
+
if (typeof message.role !== "string") return undefined;
|
|
52
|
+
const text = messageText(message.parts);
|
|
53
|
+
const base =
|
|
54
|
+
typeof message.kind === "string"
|
|
55
|
+
? { kind: message.kind, role: message.role }
|
|
56
|
+
: { role: message.role };
|
|
57
|
+
const candidate = (length: number): string =>
|
|
58
|
+
JSON.stringify([
|
|
59
|
+
{
|
|
60
|
+
...base,
|
|
61
|
+
parts: [{ content: `${text.slice(0, length)}${TRUNCATION_MARKER}`, type: "text" }],
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
let best: string | undefined;
|
|
65
|
+
let low = 1;
|
|
66
|
+
let high = text.length;
|
|
67
|
+
while (low <= high) {
|
|
68
|
+
const middle = Math.floor((low + high) / 2);
|
|
69
|
+
const probe = candidate(middle);
|
|
70
|
+
if (byteLength(probe) <= budgetBytes) {
|
|
71
|
+
best = probe;
|
|
72
|
+
low = middle + 1;
|
|
73
|
+
} else {
|
|
74
|
+
high = middle - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (best !== undefined) return best;
|
|
78
|
+
const minimal = candidate(0);
|
|
79
|
+
return byteLength(minimal) <= budgetBytes ? minimal : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Adds truncated copies of the turn's content attributes back into a stripped
|
|
84
|
+
* checkpoint, sized so the whole record stays within the snapshot budgets. A
|
|
85
|
+
* copy that cannot fit small enough is left out; the capture-policy attributes
|
|
86
|
+
* stay truthful either way.
|
|
87
|
+
*/
|
|
88
|
+
function withFittedTurnContent(
|
|
89
|
+
attributes: Record<string, unknown>,
|
|
90
|
+
original: Readonly<Record<string, unknown>>,
|
|
91
|
+
record: ScopeRecord,
|
|
92
|
+
): Record<string, unknown> {
|
|
93
|
+
let fitted = attributes;
|
|
94
|
+
for (const key of TURN_CONTENT_ATTRIBUTES) {
|
|
95
|
+
const value = original[key];
|
|
96
|
+
if (typeof value !== "string") continue;
|
|
97
|
+
const withoutKey = { ...fitted };
|
|
98
|
+
delete withoutKey[key];
|
|
99
|
+
const bare = { ...record, attributes: withoutKey } as ScopeRecord;
|
|
100
|
+
let budget = Math.min(
|
|
101
|
+
SERIALIZED_BYTES - byteLength(JSON.stringify(bare.data)),
|
|
102
|
+
SNAPSHOT_BYTES - nodeBytes(bare),
|
|
103
|
+
);
|
|
104
|
+
while (budget > 0) {
|
|
105
|
+
const truncated = fitTurnContent(value, budget);
|
|
106
|
+
// Shrinking the budget cannot help when even the minimal form does not
|
|
107
|
+
// fit or the copy is not truncatable JSON.
|
|
108
|
+
if (truncated === undefined) break;
|
|
109
|
+
const candidate = { ...fitted, [key]: truncated };
|
|
110
|
+
if (nodeBytes({ ...record, attributes: candidate } as ScopeRecord) <= SNAPSHOT_BYTES) {
|
|
111
|
+
fitted = candidate;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
budget = Math.floor(budget / 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return fitted;
|
|
118
|
+
}
|
|
8
119
|
|
|
9
120
|
import type { TraceSnapshot } from "./types.js";
|
|
10
121
|
export function boundedSerializer(
|
|
@@ -108,10 +219,17 @@ export function validSnapshot(
|
|
|
108
219
|
step: [],
|
|
109
220
|
model: ["provider", "modelId"],
|
|
110
221
|
tool: ["callId", "name"],
|
|
111
|
-
approval: ["requestId"
|
|
222
|
+
approval: ["requestId"],
|
|
112
223
|
memory: ["operation", "phase", "slot", "storeId"],
|
|
113
224
|
};
|
|
114
225
|
if (!fields[record.data.type].every((key) => typeof options[key] === "string")) return false;
|
|
226
|
+
if (
|
|
227
|
+
record.data.type === "approval" &&
|
|
228
|
+
!["kind", "connectionName", "callId", "toolName"].every(
|
|
229
|
+
(key) => options[key] === undefined || typeof options[key] === "string",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
return false;
|
|
115
233
|
if (record.data.type === "activation" && !Number.isSafeInteger(options.sequence)) return false;
|
|
116
234
|
if (record.data.type === "step" && !Number.isSafeInteger(options.index)) return false;
|
|
117
235
|
if (
|
|
@@ -201,15 +319,25 @@ export function snapshotRecord(record: ScopeRecord, serializer: ContentSerialize
|
|
|
201
319
|
json = serializer.json(data);
|
|
202
320
|
} catch {}
|
|
203
321
|
const fallback = () => {
|
|
204
|
-
|
|
322
|
+
const declined = {
|
|
205
323
|
emit: record.capture.emit,
|
|
206
324
|
recordInputs: false,
|
|
207
325
|
recordOutputs: false,
|
|
208
|
-
}
|
|
326
|
+
};
|
|
327
|
+
let structural = capturedScopeData(data, declined);
|
|
209
328
|
if (structural.type === "step")
|
|
210
329
|
structural = { ...structural, options: { ...structural.options, runtimeContext: undefined } };
|
|
211
330
|
if (structural.type === "model")
|
|
212
331
|
structural = { ...structural, options: { ...structural.options, runtimeContext: undefined } };
|
|
332
|
+
// A size-driven strip is not a capture-policy decline: the policy
|
|
333
|
+
// attributes keep the record's real decision, and the turn's content
|
|
334
|
+
// attributes stay as truncated copies when they fit the node budget.
|
|
335
|
+
const original = record.attributes ?? {};
|
|
336
|
+
const attributes = withFittedTurnContent(
|
|
337
|
+
preservingCapturePolicy(withoutDeclinedContent(original, declined) ?? {}, original),
|
|
338
|
+
original,
|
|
339
|
+
{ ...record, version: 1, data: structural },
|
|
340
|
+
);
|
|
213
341
|
return JSON.parse(
|
|
214
342
|
JSON.stringify({
|
|
215
343
|
...record,
|
|
@@ -224,11 +352,7 @@ export function snapshotRecord(record: ScopeRecord, serializer: ContentSerialize
|
|
|
224
352
|
errorCode: record.terminal.errorCode,
|
|
225
353
|
usage: record.terminal.usage,
|
|
226
354
|
},
|
|
227
|
-
attributes
|
|
228
|
-
withoutDeclinedContent(record.attributes ?? {}, {
|
|
229
|
-
recordInputs: false,
|
|
230
|
-
recordOutputs: false,
|
|
231
|
-
}) ?? record.attributes,
|
|
355
|
+
attributes,
|
|
232
356
|
}),
|
|
233
357
|
) as ScopeRecord;
|
|
234
358
|
};
|
package/src/core/span-kinds.ts
CHANGED
|
@@ -238,19 +238,23 @@ const kinds: { [K in ScopeData["type"]]: Kind<K> } = {
|
|
|
238
238
|
},
|
|
239
239
|
},
|
|
240
240
|
approval: {
|
|
241
|
-
parents: ["tool"],
|
|
241
|
+
parents: ["tool", "activation"],
|
|
242
242
|
prepare({ options }, context) {
|
|
243
243
|
return {
|
|
244
244
|
name: SPAN_NAMES.approval,
|
|
245
245
|
attributes: {
|
|
246
246
|
...context.attributes,
|
|
247
247
|
...frameworkAttributes(context.identity.framework),
|
|
248
|
-
|
|
248
|
+
// A wait the turn holds belongs to no step.
|
|
249
|
+
...(options.callId === undefined
|
|
250
|
+
? { "agent.turn.id": context.attempt.turnId }
|
|
251
|
+
: attemptAttributes(context)),
|
|
249
252
|
...namingAttributes(SPAN_NAMES.approval, "workflow"),
|
|
250
253
|
"gen_ai.operation.name": "workflow",
|
|
251
254
|
"gen_ai.tool.call.id": options.callId,
|
|
252
255
|
"agent.action.name": options.toolName,
|
|
253
|
-
"agent.approval.kind": "tool-approval",
|
|
256
|
+
"agent.approval.kind": options.kind ?? "tool-approval",
|
|
257
|
+
"agent.connection.name": options.connectionName,
|
|
254
258
|
"agent.approval.request_id": options.requestId,
|
|
255
259
|
[CONTENT_FIELDS.approvalRequest]: context.capture.recordInputs
|
|
256
260
|
? context.serializer.json(options.request)
|
package/src/core/types.ts
CHANGED
|
@@ -202,20 +202,26 @@ export interface ToolOptions {
|
|
|
202
202
|
readonly kind?: ToolKind;
|
|
203
203
|
readonly arguments?: unknown;
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* A wait on a person. Under a tool call it names that call; under a turn it has no call, such
|
|
207
|
+
* as a budget prompt or a sign-in the turn needs before it can run.
|
|
208
|
+
*/
|
|
209
|
+
export interface ApprovalOptions {
|
|
210
|
+
readonly requestId: string;
|
|
211
|
+
readonly request?: unknown;
|
|
212
|
+
/** What the person is asked, recorded as `agent.approval.kind`. Defaults to `tool-approval`. */
|
|
213
|
+
readonly kind?: string;
|
|
214
|
+
/** The connection a sign-in is for, recorded as `agent.connection.name`. */
|
|
215
|
+
readonly connectionName?: string;
|
|
216
|
+
readonly callId?: string;
|
|
217
|
+
readonly toolName?: string;
|
|
218
|
+
}
|
|
205
219
|
export type ScopeData =
|
|
206
220
|
| { readonly type: "activation"; readonly options: TurnMetadata }
|
|
207
221
|
| { readonly type: "step"; readonly options: StepOptions }
|
|
208
222
|
| { readonly type: "model"; readonly options: ModelOptions }
|
|
209
223
|
| { readonly type: "tool"; readonly options: ToolOptions }
|
|
210
|
-
| {
|
|
211
|
-
readonly type: "approval";
|
|
212
|
-
readonly options: {
|
|
213
|
-
requestId: string;
|
|
214
|
-
request?: unknown;
|
|
215
|
-
callId: string;
|
|
216
|
-
toolName: string;
|
|
217
|
-
};
|
|
218
|
-
}
|
|
224
|
+
| { readonly type: "approval"; readonly options: ApprovalOptions }
|
|
219
225
|
| {
|
|
220
226
|
readonly type: "memory";
|
|
221
227
|
readonly options: {
|
package/src/index.ts
CHANGED
package/src/operations.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface TurnOperation extends Operation {
|
|
|
52
52
|
/** Replaces the turn's links before its deferred span is exported. */
|
|
53
53
|
links(links: readonly TraceLink[]): void;
|
|
54
54
|
memory: WrappedOperation<MemoryInput, MemoryOperation>;
|
|
55
|
+
/** A wait on a person that no tool call owns, such as a budget prompt or a sign-in. */
|
|
56
|
+
approval: WrappedOperation<ApprovalInput, ApprovalOperation>;
|
|
57
|
+
/** Returns an open turn-level approval without starting one. Durable turns only. */
|
|
58
|
+
findApproval(requestId: string): ApprovalOperation | undefined;
|
|
59
|
+
}
|
|
60
|
+
/** What a person is asked. `kind` defaults to `tool-approval`. */
|
|
61
|
+
export interface ApprovalInput {
|
|
62
|
+
readonly requestId: string;
|
|
63
|
+
readonly request?: unknown;
|
|
64
|
+
readonly kind?: string;
|
|
65
|
+
readonly connectionName?: string;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* One tool call. In a durable turn, calling `tool` again with the same `callId`
|
|
@@ -77,7 +88,7 @@ export interface AttemptOperation extends Operation {
|
|
|
77
88
|
memory: WrappedOperation<MemoryInput, MemoryOperation>;
|
|
78
89
|
}
|
|
79
90
|
export interface ToolOperation extends Operation {
|
|
80
|
-
approval: WrappedOperation<
|
|
91
|
+
approval: WrappedOperation<ApprovalInput, ApprovalOperation>;
|
|
81
92
|
/** Returns an open approval without starting one. Durable turns only. */
|
|
82
93
|
findApproval(requestId: string): ApprovalOperation | undefined;
|
|
83
94
|
/** A tool call made while this tool runs, such as a connector's nested call. */
|
|
@@ -200,10 +211,20 @@ export function operationHandle(
|
|
|
200
211
|
undefined,
|
|
201
212
|
onError,
|
|
202
213
|
);
|
|
214
|
+
const approvals = (call?: { callId: string; toolName: string }) => ({
|
|
215
|
+
findApproval: (requestId: string) =>
|
|
216
|
+
found<ApprovalOperation>(runtime.find({ type: "approval", options: { requestId, ...call } })),
|
|
217
|
+
approval: wrappedOperation(
|
|
218
|
+
async (input: ApprovalInput) => child({ type: "approval", options: { ...input, ...call } }),
|
|
219
|
+
{ outcome: "ignored" },
|
|
220
|
+
onError,
|
|
221
|
+
),
|
|
222
|
+
});
|
|
203
223
|
if (data.type === "activation")
|
|
204
224
|
return live({
|
|
205
225
|
...common,
|
|
206
226
|
memory,
|
|
227
|
+
...approvals(),
|
|
207
228
|
links: (links: readonly TraceLink[]) => runtime.update({ links }),
|
|
208
229
|
findAttempt: (input: { stepIndex: number; attempt: number }) =>
|
|
209
230
|
found<AttemptOperation>(
|
|
@@ -309,22 +330,7 @@ export function operationHandle(
|
|
|
309
330
|
...common,
|
|
310
331
|
memory,
|
|
311
332
|
tool,
|
|
312
|
-
|
|
313
|
-
found<ApprovalOperation>(
|
|
314
|
-
runtime.find({
|
|
315
|
-
type: "approval",
|
|
316
|
-
options: { requestId, callId: data.options.callId, toolName: data.options.name },
|
|
317
|
-
}),
|
|
318
|
-
),
|
|
319
|
-
approval: wrappedOperation(
|
|
320
|
-
async (input: { requestId: string; request?: unknown }) =>
|
|
321
|
-
child({
|
|
322
|
-
type: "approval",
|
|
323
|
-
options: { ...input, callId: data.options.callId, toolName: data.options.name },
|
|
324
|
-
}),
|
|
325
|
-
{ outcome: "ignored" },
|
|
326
|
-
onError,
|
|
327
|
-
),
|
|
333
|
+
...approvals({ callId: data.options.callId, toolName: data.options.name }),
|
|
328
334
|
} as ToolOperation);
|
|
329
335
|
return common;
|
|
330
336
|
}
|