agents
Version:
A home for your AI agents
367 lines (366 loc) • 13.3 kB
JavaScript
import { AGENT_TOOL_MILESTONE_PART, AGENT_TOOL_PROGRESS_PART, isAgentToolLifecycleChunk } from "./agent-tool-types.js";
import { t as applyChunkToParts } from "./message-builder-DAl9AhHH.js";
//#region src/chat/chunk-size.ts
const textEncoder = new TextEncoder();
/** Maximum serialized chunk body size before skipping storage (bytes). */
const CHUNK_MAX_BYTES = 18e5;
/** Byte size of a chunk body in its stored (JSON-escaped) encoding. */
function storedChunkBytes(body) {
return textEncoder.encode(JSON.stringify(body)).byteLength;
}
/**
* Whether `ResumableStream.storeChunk` skips storing this body: it is still
* broadcast live, but absent from replay.
*/
function isChunkTooLargeToStore(body) {
return storedChunkBytes(body) > CHUNK_MAX_BYTES;
}
//#endregion
//#region src/chat/agent-tools.ts
/**
* Pull a reserved `data-agent-progress` chunk (emitted by a running sub-agent's
* `reportProgress`) into a latest-wins snapshot. Returns `undefined` for any
* other chunk so the caller keeps the prior snapshot.
*/
function readAgentToolProgressChunk(chunk) {
if (typeof chunk !== "object" || chunk === null || chunk.type !== "data-agent-progress") return;
const data = chunk.data ?? {};
return {
...typeof data.fraction === "number" ? { fraction: data.fraction } : {},
...typeof data.message === "string" ? { message: data.message } : {},
...typeof data.phase === "string" ? { phase: data.phase } : {},
...data.data !== void 0 ? { data: data.data } : {},
at: Date.now()
};
}
/**
* Pull a reserved `data-agent-milestone` chunk into a durable milestone record,
* or `undefined` for any other chunk. Milestones carry their own monotonic
* `sequence` so the caller can dedupe replay-vs-live races.
*/
function readAgentToolMilestoneChunk(chunk) {
if (typeof chunk !== "object" || chunk === null || chunk.type !== "data-agent-milestone") return;
const data = chunk.data ?? {};
if (typeof data.name !== "string") return void 0;
return {
name: data.name,
sequence: typeof data.sequence === "number" ? data.sequence : 0,
at: typeof data.at === "number" ? data.at : Date.now(),
...data.data !== void 0 ? { data: data.data } : {}
};
}
/**
* Merge a milestone into a run's ordered milestone list, deduping on `sequence`
* (idempotent across replay + live races) and keeping the list sorted.
*/
function mergeMilestone(existing, milestone) {
if (existing?.some((m) => m.sequence === milestone.sequence)) return existing;
const list = existing ? [...existing, milestone] : [milestone];
list.sort((a, b) => a.sequence - b.sequence);
return list;
}
/** Latest-wins coalescing window for `reportProgress` emits (per run). */
const AGENT_TOOL_PROGRESS_COALESCE_MS = 200;
/**
* Shared implementation of `reportProgress` for chat hosts. Builds the reserved
* transient `data-agent-progress` wire frame, coalesces bursts to a bounded
* cadence (latest-wins; a `fraction >= 1` "done" frame always flushes), and
* persists a latest snapshot. `data` rides the live frame but is only persisted
* when the caller opts in via `{ persist: true }`.
*/
var AgentToolProgressEmitter = class {
constructor(hooks) {
this.hooks = hooks;
this._lastEmitAt = /* @__PURE__ */ new Map();
}
report(progress, options) {
const active = this.hooks.resolveActiveRun();
if (!active) return "inactive";
const { runId, requestId } = active;
const now = Date.now();
if (typeof progress.milestone === "string" && progress.milestone) {
this._lastEmitAt.set(runId, now);
const sequence = this.hooks.persistMilestone(runId, progress.milestone, progress.data, now);
this.hooks.broadcast(requestId, JSON.stringify({
type: AGENT_TOOL_MILESTONE_PART,
data: {
name: progress.milestone,
sequence,
at: now,
...typeof progress.fraction === "number" ? { fraction: progress.fraction } : {},
...typeof progress.message === "string" ? { message: progress.message } : {},
...typeof progress.phase === "string" ? { phase: progress.phase } : {},
...progress.data !== void 0 ? { data: progress.data } : {}
}
}));
return "emitted";
}
const last = this._lastEmitAt.get(runId) ?? 0;
const isDone = typeof progress.fraction === "number" && progress.fraction >= 1;
if (now - last < AGENT_TOOL_PROGRESS_COALESCE_MS && !isDone) return "coalesced";
this._lastEmitAt.set(runId, now);
const wire = {
...typeof progress.fraction === "number" ? { fraction: progress.fraction } : {},
...typeof progress.message === "string" ? { message: progress.message } : {},
...typeof progress.phase === "string" ? { phase: progress.phase } : {},
...progress.data !== void 0 ? { data: progress.data } : {}
};
this.hooks.broadcast(requestId, JSON.stringify({
type: AGENT_TOOL_PROGRESS_PART,
id: crypto.randomUUID(),
transient: true,
data: wire
}));
this.hooks.persistSnapshot(runId, {
...typeof progress.fraction === "number" ? { fraction: progress.fraction } : {},
...typeof progress.message === "string" ? { message: progress.message } : {},
...typeof progress.phase === "string" ? { phase: progress.phase } : {},
...options?.persist && progress.data !== void 0 ? { data: progress.data } : {}
}, now);
return "emitted";
}
/** Drop coalescing state for a settled run (called on terminal). */
forget(runId) {
this._lastEmitAt.delete(runId);
}
};
function sortRuns(runs) {
return [...runs].sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
return a.runId.localeCompare(b.runId);
});
}
function rebuildIndexes(runsById) {
const grouped = {};
const unboundRuns = [];
for (const run of Object.values(runsById)) if (run.parentToolCallId) {
grouped[run.parentToolCallId] = grouped[run.parentToolCallId] ?? [];
grouped[run.parentToolCallId].push(run);
} else unboundRuns.push(run);
for (const [toolCallId, runs] of Object.entries(grouped)) grouped[toolCallId] = sortRuns(runs);
return {
runsByToolCallId: grouped,
unboundRuns: sortRuns(unboundRuns)
};
}
function emptyRun(message) {
const { event } = message;
if (event.kind === "started") return {
runId: event.runId,
agentType: event.agentType,
parentToolCallId: message.parentToolCallId,
inputPreview: event.inputPreview,
order: event.order,
display: event.display,
status: "running",
parts: [],
subAgent: {
agent: event.agentType,
name: event.runId
}
};
}
function applyToRun(prev, message) {
const seeded = prev ?? emptyRun(message);
const { event } = message;
switch (event.kind) {
case "started":
if (seeded?.status === "completed" || seeded?.status === "error" || seeded?.status === "aborted" || seeded?.status === "interrupted") return seeded;
return {
...seeded,
runId: event.runId,
agentType: event.agentType,
parentToolCallId: message.parentToolCallId,
inputPreview: event.inputPreview,
order: event.order,
display: event.display,
status: "running",
parts: seeded?.parts ?? [],
subAgent: {
agent: event.agentType,
name: event.runId
}
};
case "chunk": {
if (!seeded) return void 0;
const parts = seeded.parts.map((part) => ({ ...part }));
let parsed;
try {
parsed = JSON.parse(event.body);
applyChunkToParts(parts, parsed);
} catch {
return seeded;
}
const progress = readAgentToolProgressChunk(parsed);
if (progress) return {
...seeded,
parts,
progress
};
const milestone = readAgentToolMilestoneChunk(parsed);
if (milestone) {
const milestones = mergeMilestone(seeded.milestones, milestone);
const isNew = milestones !== seeded.milestones;
const notOlder = seeded.progress === void 0 || milestone.at >= seeded.progress.at;
if (!isNew || !notOlder) return {
...seeded,
parts,
milestones
};
const data = parsed.data ?? {};
const snapshot = {
...typeof data.fraction === "number" ? { fraction: data.fraction } : {},
...typeof data.message === "string" ? { message: data.message } : {},
...typeof data.phase === "string" ? { phase: data.phase } : {},
milestone: milestone.name,
at: milestone.at
};
return {
...seeded,
parts,
progress: snapshot,
milestones
};
}
return {
...seeded,
parts
};
}
case "finished":
if (!seeded) return void 0;
return {
...seeded,
status: "completed",
summary: event.summary,
error: void 0
};
case "error":
if (!seeded) return void 0;
return {
...seeded,
status: "error",
error: event.error
};
case "aborted":
if (!seeded) return void 0;
return {
...seeded,
status: "aborted",
error: event.reason
};
case "interrupted":
if (!seeded) return void 0;
return {
...seeded,
status: "interrupted",
error: event.error,
reason: event.reason,
childStillRunning: event.childStillRunning
};
}
}
function createAgentToolEventState() {
return {
runsById: {},
runsByToolCallId: {},
unboundRuns: []
};
}
/**
* Identity of an agent-tool event for replay-vs-live dedupe. Ordinary chunks
* key on the broadcast `sequence`, which is their stored-chunk position on
* both the live and replay paths. Lifecycle events key on their content: a
* reattached run can be interrupted again with a different reason, and the
* reducer overwrites on lifecycle events, so re-applying one is harmless.
* Milestones key on their own persisted sequence. Progress frames and chunks
* too large to store reuse the next stored chunk's sequence without consuming
* it and are never replayed, so they key on their own emitter-assigned id.
*/
function agentToolEventDedupeKey(message) {
const { event } = message;
let identity;
if (event.kind !== "chunk") identity = `event:${JSON.stringify(event)}`;
else if (event.unstoredId !== void 0) identity = `unstored:${event.unstoredId}`;
else {
let milestone;
let progressId;
let progress = false;
if (event.body.includes("data-agent-milestone") || event.body.includes("data-agent-progress")) try {
const parsed = JSON.parse(event.body);
milestone = readAgentToolMilestoneChunk(parsed);
progress = readAgentToolProgressChunk(parsed) !== void 0;
const id = parsed.id;
if (typeof id === "string") progressId = id;
} catch {
milestone = void 0;
}
identity = milestone ? `milestone:${milestone.sequence}` : progress ? progressId !== void 0 ? `progress:${progressId}` : `progress:${message.sequence}:${event.body}` : `seq:${message.sequence}`;
}
return [
message.parentToolCallId ?? "",
event.runId,
identity
].join("\0");
}
function applyAgentToolEvent(state, message) {
if (message.type !== "agent-tool-event") return state;
const runId = message.event.runId;
const nextRun = applyToRun(state.runsById[runId], message);
if (!nextRun) return state;
const runsById = {
...state.runsById,
[runId]: nextRun
};
return {
runsById,
...rebuildIndexes(runsById)
};
}
/**
* Snoop a host's outgoing chat frames while any agent-tool run is in flight and
* forward the owning run's streamed body to its live tailers (or capture its
* error), without altering the frame — the caller still broadcasts it (#1575).
*
* Shared verbatim by `@cloudflare/ai-chat` and `@cloudflare/think`; the only
* per-host variance (the response-frame type constant and the run-lookup, whose
* SQL differs) is supplied via {@link AgentToolBroadcastHooks}. Inspection runs
* for a run's whole lifecycle (live sequences exist even with no tailer), so
* error capture never depends on tailer timing. A frame belongs to a run iff it
* carries that run's turn request id, so concurrent runs can't cross-contaminate
* each other's progress or error state.
*
* Returns the run id when the frame is one of that run's content chunks, so a
* host can skip broadcasting chunks no client is watching.
*/
function interceptAgentToolBroadcast(msg, hooks) {
if ((hooks.forwarders.size > 0 || hooks.liveSequences.size > 0 || (hooks.terminalOnlyRuns?.size ?? 0) > 0) && typeof msg === "string") try {
const parsed = JSON.parse(msg);
if (parsed.type === hooks.responseType && typeof parsed.id === "string") {
const runId = hooks.runForRequest(parsed.id);
if (runId !== null) {
if (parsed.error === true && typeof parsed.body === "string") {
hooks.lastErrors.set(runId, parsed.body);
hooks.onError?.(runId, parsed.body);
} else if (typeof parsed.body === "string" && parsed.body.length > 0) {
const sequence = hooks.liveSequences.get(runId) ?? 0;
const lifecycle = isAgentToolLifecycleChunk(parsed.body);
const unstored = !lifecycle && isChunkTooLargeToStore(parsed.body);
hooks.liveSequences.set(runId, lifecycle || unstored ? sequence : sequence + 1);
const chunk = {
sequence,
body: parsed.body,
...unstored ? { unstoredId: crypto.randomUUID() } : {}
};
const forwarders = hooks.forwarders.get(runId);
if (forwarders) for (const forward of forwarders) forward(chunk);
return runId;
}
}
}
} catch {}
return null;
}
//#endregion
export { interceptAgentToolBroadcast as a, createAgentToolEventState as i, agentToolEventDedupeKey as n, CHUNK_MAX_BYTES as o, applyAgentToolEvent as r, storedChunkBytes as s, AgentToolProgressEmitter as t };
//# sourceMappingURL=agent-tools-CdJC-8Dc.js.map