@superdurable/dex-agents 0.0.0-stage → 0.2.0
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/LICENSE +105 -0
- package/README.md +1981 -2
- package/dist/src/agent-built-in-tools.d.ts +81 -0
- package/dist/src/agent-built-in-tools.js +246 -0
- package/dist/src/agent-built-in-tools.js.map +1 -0
- package/dist/src/agent-context-compaction.d.ts +108 -0
- package/dist/src/agent-context-compaction.js +39 -0
- package/dist/src/agent-context-compaction.js.map +1 -0
- package/dist/src/agent-event-writer.d.ts +26 -0
- package/dist/src/agent-event-writer.js +110 -0
- package/dist/src/agent-event-writer.js.map +1 -0
- package/dist/src/agent-framework-runner.d.ts +808 -0
- package/dist/src/agent-framework-runner.js +82 -0
- package/dist/src/agent-framework-runner.js.map +1 -0
- package/dist/src/agent-plans.d.ts +48 -0
- package/dist/src/agent-plans.js +86 -0
- package/dist/src/agent-plans.js.map +1 -0
- package/dist/src/agent-session-cancellation-steps.d.ts +28 -0
- package/dist/src/agent-session-cancellation-steps.js +154 -0
- package/dist/src/agent-session-cancellation-steps.js.map +1 -0
- package/dist/src/agent-session-codecs.d.ts +105 -0
- package/dist/src/agent-session-codecs.js +689 -0
- package/dist/src/agent-session-codecs.js.map +1 -0
- package/dist/src/agent-session-flow.d.ts +416 -0
- package/dist/src/agent-session-flow.js +623 -0
- package/dist/src/agent-session-flow.js.map +1 -0
- package/dist/src/agent-session-persistence.d.ts +133 -0
- package/dist/src/agent-session-persistence.js +74 -0
- package/dist/src/agent-session-persistence.js.map +1 -0
- package/dist/src/agent-session-settings.d.ts +58 -0
- package/dist/src/agent-session-settings.js +81 -0
- package/dist/src/agent-session-settings.js.map +1 -0
- package/dist/src/agent-session-step-options.d.ts +27 -0
- package/dist/src/agent-session-step-options.js +39 -0
- package/dist/src/agent-session-step-options.js.map +1 -0
- package/dist/src/agent-session-step-types.d.ts +20 -0
- package/dist/src/agent-session-step-types.js +28 -0
- package/dist/src/agent-session-step-types.js.map +1 -0
- package/dist/src/agent-session-steps.d.ts +179 -0
- package/dist/src/agent-session-steps.js +1272 -0
- package/dist/src/agent-session-steps.js.map +1 -0
- package/dist/src/agent-session-tracer.d.ts +61 -0
- package/dist/src/agent-session-tracer.js +608 -0
- package/dist/src/agent-session-tracer.js.map +1 -0
- package/dist/src/agent-session-transitions.d.ts +224 -0
- package/dist/src/agent-session-transitions.js +850 -0
- package/dist/src/agent-session-transitions.js.map +1 -0
- package/dist/src/agent-session-values.d.ts +998 -0
- package/dist/src/agent-session-values.js +162 -0
- package/dist/src/agent-session-values.js.map +1 -0
- package/dist/src/agent-session-wait-steps.d.ts +53 -0
- package/dist/src/agent-session-wait-steps.js +389 -0
- package/dist/src/agent-session-wait-steps.js.map +1 -0
- package/dist/src/agent-tracing-options.d.ts +107 -0
- package/dist/src/agent-tracing-options.js +20 -0
- package/dist/src/agent-tracing-options.js.map +1 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +40 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.js +235 -0
- package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -0
- package/dist/src/ai-sdk/index.d.ts +1 -0
- package/dist/src/ai-sdk/index.js +9 -0
- package/dist/src/ai-sdk/index.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/index.d.ts +3 -0
- package/dist/src/mcp/index.js +10 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.d.ts +29 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.js +185 -0
- package/dist/src/mcp/mcp-resource-and-prompt-tools.js.map +1 -0
- package/dist/src/mcp/mcp-server-config.d.ts +86 -0
- package/dist/src/mcp/mcp-server-config.js +38 -0
- package/dist/src/mcp/mcp-server-config.js.map +1 -0
- package/dist/src/mcp/mcp-tool-source.d.ts +470 -0
- package/dist/src/mcp/mcp-tool-source.js +1204 -0
- package/dist/src/mcp/mcp-tool-source.js.map +1 -0
- package/dist/src/stepwise/agent-definition.d.ts +158 -0
- package/dist/src/stepwise/agent-definition.js +9 -0
- package/dist/src/stepwise/agent-definition.js.map +1 -0
- package/dist/src/stepwise/function-tool.d.ts +130 -0
- package/dist/src/stepwise/function-tool.js +75 -0
- package/dist/src/stepwise/function-tool.js.map +1 -0
- package/dist/src/stepwise/index.d.ts +7 -0
- package/dist/src/stepwise/index.js +12 -0
- package/dist/src/stepwise/index.js.map +1 -0
- package/dist/src/stepwise/json-schema-validation.d.ts +9 -0
- package/dist/src/stepwise/json-schema-validation.js +141 -0
- package/dist/src/stepwise/json-schema-validation.js.map +1 -0
- package/dist/src/stepwise/model-client.d.ts +194 -0
- package/dist/src/stepwise/model-client.js +9 -0
- package/dist/src/stepwise/model-client.js.map +1 -0
- package/dist/src/stepwise/scripted-model-client.d.ts +99 -0
- package/dist/src/stepwise/scripted-model-client.js +156 -0
- package/dist/src/stepwise/scripted-model-client.js.map +1 -0
- package/dist/src/stepwise/stepwise-agent-runner.d.ts +189 -0
- package/dist/src/stepwise/stepwise-agent-runner.js +804 -0
- package/dist/src/stepwise/stepwise-agent-runner.js.map +1 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +35 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.js +56 -0
- package/dist/src/stepwise/stepwise-agent-session-flow.js.map +1 -0
- package/dist/src/stepwise/stepwise-input-items.d.ts +12 -0
- package/dist/src/stepwise/stepwise-input-items.js +172 -0
- package/dist/src/stepwise/stepwise-input-items.js.map +1 -0
- package/dist/src/stepwise/tool-source.d.ts +103 -0
- package/dist/src/stepwise/tool-source.js +9 -0
- package/dist/src/stepwise/tool-source.js.map +1 -0
- package/dist/src/testing/agent-session-test-client.d.ts +205 -0
- package/dist/src/testing/agent-session-test-client.js +290 -0
- package/dist/src/testing/agent-session-test-client.js.map +1 -0
- package/dist/src/testing/agent-test-environment.d.ts +52 -0
- package/dist/src/testing/agent-test-environment.js +86 -0
- package/dist/src/testing/agent-test-environment.js.map +1 -0
- package/dist/src/testing/agent-worker-process.d.ts +91 -0
- package/dist/src/testing/agent-worker-process.js +150 -0
- package/dist/src/testing/agent-worker-process.js.map +1 -0
- package/dist/src/testing/child-process-group.d.ts +33 -0
- package/dist/src/testing/child-process-group.js +164 -0
- package/dist/src/testing/child-process-group.js.map +1 -0
- package/dist/src/testing/coordination.d.ts +99 -0
- package/dist/src/testing/coordination.js +155 -0
- package/dist/src/testing/coordination.js.map +1 -0
- package/dist/src/testing/index.d.ts +8 -0
- package/dist/src/testing/index.js +16 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/polling.d.ts +31 -0
- package/dist/src/testing/polling.js +51 -0
- package/dist/src/testing/polling.js.map +1 -0
- package/dist/src/testing/private-dex-engine.d.ts +101 -0
- package/dist/src/testing/private-dex-engine.js +223 -0
- package/dist/src/testing/private-dex-engine.js.map +1 -0
- package/dist/src/testing/scripted-model-client.d.ts +1 -0
- package/dist/src/testing/scripted-model-client.js +10 -0
- package/dist/src/testing/scripted-model-client.js.map +1 -0
- package/dist/src/testing/step-history.d.ts +40 -0
- package/dist/src/testing/step-history.js +59 -0
- package/dist/src/testing/step-history.js.map +1 -0
- package/dist/src/ui-stream/agent-event-json.d.ts +30 -0
- package/dist/src/ui-stream/agent-event-json.js +64 -0
- package/dist/src/ui-stream/agent-event-json.js.map +1 -0
- package/dist/src/ui-stream/agent-event-sse.d.ts +154 -0
- package/dist/src/ui-stream/agent-event-sse.js +159 -0
- package/dist/src/ui-stream/agent-event-sse.js.map +1 -0
- package/dist/src/ui-stream/agent-event-stream-positions.d.ts +42 -0
- package/dist/src/ui-stream/agent-event-stream-positions.js +153 -0
- package/dist/src/ui-stream/agent-event-stream-positions.js.map +1 -0
- package/dist/src/ui-stream/agent-session-record-reader.d.ts +72 -0
- package/dist/src/ui-stream/agent-session-record-reader.js +241 -0
- package/dist/src/ui-stream/agent-session-record-reader.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.d.ts +219 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.js +440 -0
- package/dist/src/ui-stream/agent-ui-message-stream-responses.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-stream.d.ts +138 -0
- package/dist/src/ui-stream/agent-ui-message-stream.js +681 -0
- package/dist/src/ui-stream/agent-ui-message-stream.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-message-types.d.ts +238 -0
- package/dist/src/ui-stream/agent-ui-message-types.js +57 -0
- package/dist/src/ui-stream/agent-ui-message-types.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-messages.d.ts +66 -0
- package/dist/src/ui-stream/agent-ui-messages.js +220 -0
- package/dist/src/ui-stream/agent-ui-messages.js.map +1 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.d.ts +64 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.js +186 -0
- package/dist/src/ui-stream/agent-ui-stream-cursor.js.map +1 -0
- package/dist/src/ui-stream/index.d.ts +14 -0
- package/dist/src/ui-stream/index.js +22 -0
- package/dist/src/ui-stream/index.js.map +1 -0
- package/dist/src/ui-stream/server-sent-event-responses.d.ts +6 -0
- package/dist/src/ui-stream/server-sent-event-responses.js +34 -0
- package/dist/src/ui-stream/server-sent-event-responses.js.map +1 -0
- package/package.json +86 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { type Client } from "@superdurable/dex";
|
|
2
|
+
import type { AgentFrameworkRunner, DeferredToolOutcome } from "../agent-framework-runner.js";
|
|
3
|
+
import type { AgentSessionFlow } from "../agent-session-flow.js";
|
|
4
|
+
import { type AgentUIMessageStreamOptions } from "./agent-ui-message-stream.js";
|
|
5
|
+
/**
|
|
6
|
+
* Request header name for a UI stream resume token.
|
|
7
|
+
*
|
|
8
|
+
* Send the token in this header rather than the URL: a token can reach 64 KiB, beyond many URL limits.
|
|
9
|
+
*/
|
|
10
|
+
export declare const agentResumeTokenHeaderName = "x-agent-resume-token";
|
|
11
|
+
/** {@link AgentUIMessageStreamOptions} plus headers to add to the Response. */
|
|
12
|
+
export interface AgentUIMessageStreamResponseOptions extends AgentUIMessageStreamOptions {
|
|
13
|
+
/** Extra response headers; they override the protocol headers with the same name. */
|
|
14
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
15
|
+
}
|
|
16
|
+
/** Options of {@link sendAgentUserMessageWithUIMessageStream}. */
|
|
17
|
+
export interface AgentUserMessageUIStreamOptions extends Omit<AgentUIMessageStreamResponseOptions, "resumeToken"> {
|
|
18
|
+
/** Text of the user message. */
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One tool approval decision, the content of an AI SDK `tool-approval-response`.
|
|
23
|
+
*
|
|
24
|
+
* `useChat` records it on a tool part when the application calls `addToolApprovalResponse`.
|
|
25
|
+
*/
|
|
26
|
+
export interface AgentToolApprovalResponse {
|
|
27
|
+
/** Approval ID from the `tool-approval-request` part: the `interrupt_id` of the pending interrupt. */
|
|
28
|
+
readonly approvalId: string;
|
|
29
|
+
/** Whether the tool call may run. */
|
|
30
|
+
readonly approved: boolean;
|
|
31
|
+
/** Message the model sees after a rejection. Ignored on approval. */
|
|
32
|
+
readonly reason?: string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Turn whose assistant message holds the decision, such as `"turn-3"`, or `undefined` when unknown.
|
|
35
|
+
* {@link resolveAgentToolApprovalsWithUIMessageStream} treats a decision bound to another turn than the
|
|
36
|
+
* active one as stale, so a stale browser tab cannot answer a later turn's call that reuses a tool call
|
|
37
|
+
* ID. {@link readAgentToolApprovalResponses} sets it from the message ID.
|
|
38
|
+
*/
|
|
39
|
+
readonly turnId?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Options of {@link resolveAgentToolApproval}. */
|
|
42
|
+
export interface AgentToolApprovalOptions {
|
|
43
|
+
/** Client used for the RPC. */
|
|
44
|
+
readonly client: Client;
|
|
45
|
+
/** The agent session Flow. */
|
|
46
|
+
readonly flow: AgentSessionFlow<AgentFrameworkRunner<DeferredToolOutcome>>;
|
|
47
|
+
/** Session ID: the Flow ID of the agent session. */
|
|
48
|
+
readonly flowId: string;
|
|
49
|
+
/** The decision to send. */
|
|
50
|
+
readonly approval: AgentToolApprovalResponse;
|
|
51
|
+
}
|
|
52
|
+
/** Options of {@link resolveAgentToolApprovalsWithUIMessageStream}. */
|
|
53
|
+
export interface AgentToolApprovalsUIStreamOptions extends AgentUIMessageStreamResponseOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Decisions from the browser, such as the result of {@link readAgentToolApprovalResponses}. They are
|
|
56
|
+
* untrusted: only the first decision per approval ID counts, at most 100 are read, and only interrupts
|
|
57
|
+
* pending in the session's active turn are resolved, by decisions bound to that turn or to none.
|
|
58
|
+
*/
|
|
59
|
+
readonly approvals: readonly AgentToolApprovalResponse[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns a streaming AI SDK UI message stream Response for one agent turn, or 204 when there is none.
|
|
63
|
+
*
|
|
64
|
+
* Mount it as the reconnect endpoint of `useChat`, `GET <api>/<chatId>/stream` by default, with the chat ID
|
|
65
|
+
* as the session ID; the route must check that the caller may read that session. It writes
|
|
66
|
+
* {@link streamAgentUIMessageChunks} as Server-Sent Events with the protocol headers, a keep-alive
|
|
67
|
+
* comment after each empty long poll, and `data: [DONE]` at the end. It stops reading when the browser
|
|
68
|
+
* closes the response.
|
|
69
|
+
*
|
|
70
|
+
* Use the default `DefaultChatTransport`, whose resumed streams count as replays. A stream started with a
|
|
71
|
+
* token replays the token's turn, and one started without a token replays the active turn; either way
|
|
72
|
+
* its `start` part makes the browser reset the message and rebuild it, whatever it held. With
|
|
73
|
+
* `continueAfterResumeToken`, a stream continues exactly after the last part the browser received and
|
|
74
|
+
* sends no `start` part; use it only while the browser still holds that message. Before it answers 200,
|
|
75
|
+
* the helper reads once to check the token's Dex position; a position Dex rejects restarts the turn from
|
|
76
|
+
* its first retained event.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* // Server: GET /api/chat/:id/stream
|
|
81
|
+
* export async function GET(request: Request, sessionId: string): Promise<Response> {
|
|
82
|
+
* await authorizeSession(request, sessionId);
|
|
83
|
+
* return createAgentUIMessageStreamResponse({
|
|
84
|
+
* client, flow, flowId: sessionId,
|
|
85
|
+
* resumeToken: request.headers.get(agentResumeTokenHeaderName) ?? undefined,
|
|
86
|
+
* signal: request.signal,
|
|
87
|
+
* });
|
|
88
|
+
* }
|
|
89
|
+
*
|
|
90
|
+
* // Browser
|
|
91
|
+
* let resumeToken: string | undefined = initial.resumeToken ?? undefined;
|
|
92
|
+
* const chat = useChat({
|
|
93
|
+
* id: sessionId,
|
|
94
|
+
* messages: initial.messages,
|
|
95
|
+
* resume: resumeToken !== undefined,
|
|
96
|
+
* onData: (part) => {
|
|
97
|
+
* if (part.type === "data-agent-resume-token") resumeToken = part.data as string;
|
|
98
|
+
* },
|
|
99
|
+
* transport: new DefaultChatTransport({
|
|
100
|
+
* api: "/api/chat",
|
|
101
|
+
* prepareReconnectToStreamRequest: ({ id }) => ({
|
|
102
|
+
* api: `/api/chat/${id}/stream`,
|
|
103
|
+
* headers: resumeToken === undefined ? {} : { "x-agent-resume-token": resumeToken },
|
|
104
|
+
* }),
|
|
105
|
+
* }),
|
|
106
|
+
* sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
* @param options - Session, start position, polling, and extra headers.
|
|
110
|
+
* @returns 200 with the stream; 204 when the token's turn already finished, or when the turn to replay is
|
|
111
|
+
* not active or not on the Stream; 400 with a text body when `resumeToken` is invalid.
|
|
112
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
113
|
+
* @throws {DexServiceError} When reading the start position fails.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createAgentUIMessageStreamResponse(options: AgentUIMessageStreamResponseOptions): Promise<Response>;
|
|
116
|
+
/**
|
|
117
|
+
* Queues a user message and returns the AI SDK UI message stream Response of the turn it starts.
|
|
118
|
+
*
|
|
119
|
+
* Mount it as the send endpoint of `useChat`, `POST <api>` by default, when the last message in the
|
|
120
|
+
* request body is a user message. The chat ID in the body comes from the browser, so the route must check
|
|
121
|
+
* that the caller may write to that session. The helper reads the session's turn count and the Stream
|
|
122
|
+
* position first and then calls `send_user_message`, so the stream follows the next turn that starts
|
|
123
|
+
* after the message was queued. When an earlier turn is still running, the stream waits for it to end.
|
|
124
|
+
* When other messages were queued before this one, the stream follows the turn of the oldest of them.
|
|
125
|
+
*
|
|
126
|
+
* The session's committed state ends the stream even when no event of the turn reaches the Stream, such
|
|
127
|
+
* as with the Server's Stream Store disabled: once that turn ended, the stream finishes with reason
|
|
128
|
+
* `other`, and the application reloads the stored messages. The RPC needs a running Worker.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* // POST /api/chat
|
|
133
|
+
* export async function POST(request: Request): Promise<Response> {
|
|
134
|
+
* const { id, messages } = await request.json();
|
|
135
|
+
* await authorizeSession(request, id);
|
|
136
|
+
* const lastMessage = messages.at(-1);
|
|
137
|
+
* if (lastMessage.role === "assistant") {
|
|
138
|
+
* return resolveAgentToolApprovalsWithUIMessageStream({
|
|
139
|
+
* client, flow, flowId: id, approvals: readAgentToolApprovalResponses(lastMessage) });
|
|
140
|
+
* }
|
|
141
|
+
* const text = lastMessage.parts.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
142
|
+
* return sendAgentUserMessageWithUIMessageStream({ client, flow, flowId: id, text, signal: request.signal });
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
* @param options - Session, message text, polling, and extra headers.
|
|
146
|
+
* @returns 200 with the stream; 400 with a text body when the text is blank.
|
|
147
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
148
|
+
* @throws {FlowNotActiveOrNotFoundError} When the session is not running.
|
|
149
|
+
* @throws {DexServiceError} When a read or the RPC fails.
|
|
150
|
+
*/
|
|
151
|
+
export declare function sendAgentUserMessageWithUIMessageStream(options: AgentUserMessageUIStreamOptions): Promise<Response>;
|
|
152
|
+
/**
|
|
153
|
+
* Sends one tool approval decision to the session's `resolve_interrupt` RPC.
|
|
154
|
+
*
|
|
155
|
+
* Call it with each AI SDK `tool-approval-response`: its `approvalId` is the `interrupt_id`, and its
|
|
156
|
+
* `reason` becomes the rejection message the model sees. It retries `RpcLockConflictError`, which a call
|
|
157
|
+
* that races the approval Step can raise, for up to 5 seconds. It resolves any pending interrupt of the
|
|
158
|
+
* session, so the route must check that the caller may decide for that session. The RPC needs a running
|
|
159
|
+
* Worker.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const isResolved = await resolveAgentToolApproval({
|
|
164
|
+
* client, flow, flowId: sessionId,
|
|
165
|
+
* approval: { approvalId: "call_42", approved: false, reason: "Use the sandbox account" },
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
* @param options - Session and decision.
|
|
169
|
+
* @returns `true` when the interrupt was pending and is now resolved; `false` when it is unknown, already
|
|
170
|
+
* resolved, or past its deadline.
|
|
171
|
+
* @throws {RpcLockConflictError} When the lock stays busy for 5 seconds.
|
|
172
|
+
* @throws {FlowNotActiveOrNotFoundError} When the session is not running.
|
|
173
|
+
*/
|
|
174
|
+
export declare function resolveAgentToolApproval(options: AgentToolApprovalOptions): Promise<boolean>;
|
|
175
|
+
/**
|
|
176
|
+
* Returns the tool approval decisions recorded on an AI SDK assistant message.
|
|
177
|
+
*
|
|
178
|
+
* `useChat` sets a tool part to `approval-responded` when the application calls
|
|
179
|
+
* `addToolApprovalResponse`, then sends the message list with `sendAutomaticallyWhen`. Pass the last
|
|
180
|
+
* message of that request body. The message comes from the browser, so every field is checked; parts
|
|
181
|
+
* that are not well-formed responded approvals are skipped.
|
|
182
|
+
*
|
|
183
|
+
* Each decision is bound to the message's turn when the message ID is `<turnId>-assistant`, as the agent UI
|
|
184
|
+
* streams and snapshots name assistant messages; a message with another ID gives unbound decisions.
|
|
185
|
+
*
|
|
186
|
+
* @param message - An AI SDK `UIMessage`, typically the last message of a `useChat` request body.
|
|
187
|
+
* @returns One decision per `approval-responded` tool part, in part order; empty for anything else.
|
|
188
|
+
*/
|
|
189
|
+
export declare function readAgentToolApprovalResponses(message: unknown): AgentToolApprovalResponse[];
|
|
190
|
+
/**
|
|
191
|
+
* Sends tool approval decisions and returns the AI SDK UI message stream Response that continues the turn.
|
|
192
|
+
*
|
|
193
|
+
* Mount it in the send endpoint of `useChat` for requests whose last message is an assistant message,
|
|
194
|
+
* which `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses` sends after the
|
|
195
|
+
* application answered every approval. The decisions and the chat ID come from the browser, so the route
|
|
196
|
+
* must check that the caller may decide for that session. Only interrupts that are pending in the
|
|
197
|
+
* session's active turn are resolved.
|
|
198
|
+
*
|
|
199
|
+
* The stream continues the same assistant message. A decision that is no longer pending, because another
|
|
200
|
+
* client or a deadline settled the call or its turn ended, gets the call's known outcome, or an error
|
|
201
|
+
* part when none is known, so the browser leaves `approval-responded` and does not send the decision
|
|
202
|
+
* again. Decisions bound to another turn than the active one, such as from a stale browser tab, are all
|
|
203
|
+
* stale, and the stream then ends after their outcomes. With `resumeToken`, the latest `data-agent-resume-token`, the stream continues exactly after the
|
|
204
|
+
* paused stream. Without one it reads the Stream position and the active turn's tool calls before
|
|
205
|
+
* sending the decisions. The RPCs need a running Worker.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* return resolveAgentToolApprovalsWithUIMessageStream({
|
|
210
|
+
* client, flow, flowId: id, approvals: readAgentToolApprovalResponses(messages.at(-1)) });
|
|
211
|
+
* ```
|
|
212
|
+
* @param options - Session, decisions, optional resume token, polling, and extra headers.
|
|
213
|
+
* @returns 200 with the stream, which holds only the stale decisions' outcomes and a `finish` part when
|
|
214
|
+
* no turn is active; 400 with a text body when `resumeToken` is invalid.
|
|
215
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
216
|
+
* @throws {RpcLockConflictError} When a decision's lock stays busy for 5 seconds.
|
|
217
|
+
* @throws {DexServiceError} When a read or an RPC fails.
|
|
218
|
+
*/
|
|
219
|
+
export declare function resolveAgentToolApprovalsWithUIMessageStream(options: AgentToolApprovalsUIStreamOptions): Promise<Response>;
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Sustainable Use License 1.0.
|
|
4
|
+
// You may not use this file except in compliance with the License.
|
|
5
|
+
// See the LICENSE file in the repository root.
|
|
6
|
+
//
|
|
7
|
+
// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { RpcLockConflictError } from "@superdurable/dex";
|
|
10
|
+
import { describeToolCallOutcome, isFinalToolCallStatus, } from "../agent-session-values.js";
|
|
11
|
+
import { findAgentEventResumeTokenBeforeTurn, parseTurnNumber, probeAgentEventResumeToken, readAgentEventsTailResumeToken, requireAgentEventPollTimeoutMs, } from "./agent-event-stream-positions.js";
|
|
12
|
+
import { AgentSessionRecordReader } from "./agent-session-record-reader.js";
|
|
13
|
+
import { prepareAgentUIStreamStart, readAgentUIMessageChunkBatches, } from "./agent-ui-message-stream.js";
|
|
14
|
+
import { agentResumeTokenDataPartType, agentUIMessageStreamHeaders, } from "./agent-ui-message-types.js";
|
|
15
|
+
import { createAgentUIStreamCursor, decodeAgentUIStreamCursor, encodeAgentUIStreamCursor, toolCallUIStateOfRecord, } from "./agent-ui-stream-cursor.js";
|
|
16
|
+
import { createServerSentEventResponse, mergeHeaders, serverSentEventKeepAliveFrame, } from "./server-sent-event-responses.js";
|
|
17
|
+
const lockConflictRetryTimeoutMs = 5_000;
|
|
18
|
+
const lockConflictRetryIntervalMs = 50;
|
|
19
|
+
const maximumApprovalsPerRequest = 100;
|
|
20
|
+
const maximumToolCallIdLength = 512;
|
|
21
|
+
const staleApprovalText = "The approval is no longer pending.";
|
|
22
|
+
const assistantMessageIdPattern = /^(turn-[0-9]+)-assistant$/;
|
|
23
|
+
/**
|
|
24
|
+
* Request header name for a UI stream resume token.
|
|
25
|
+
*
|
|
26
|
+
* Send the token in this header rather than the URL: a token can reach 64 KiB, beyond many URL limits.
|
|
27
|
+
*/
|
|
28
|
+
export const agentResumeTokenHeaderName = "x-agent-resume-token";
|
|
29
|
+
/**
|
|
30
|
+
* Returns a streaming AI SDK UI message stream Response for one agent turn, or 204 when there is none.
|
|
31
|
+
*
|
|
32
|
+
* Mount it as the reconnect endpoint of `useChat`, `GET <api>/<chatId>/stream` by default, with the chat ID
|
|
33
|
+
* as the session ID; the route must check that the caller may read that session. It writes
|
|
34
|
+
* {@link streamAgentUIMessageChunks} as Server-Sent Events with the protocol headers, a keep-alive
|
|
35
|
+
* comment after each empty long poll, and `data: [DONE]` at the end. It stops reading when the browser
|
|
36
|
+
* closes the response.
|
|
37
|
+
*
|
|
38
|
+
* Use the default `DefaultChatTransport`, whose resumed streams count as replays. A stream started with a
|
|
39
|
+
* token replays the token's turn, and one started without a token replays the active turn; either way
|
|
40
|
+
* its `start` part makes the browser reset the message and rebuild it, whatever it held. With
|
|
41
|
+
* `continueAfterResumeToken`, a stream continues exactly after the last part the browser received and
|
|
42
|
+
* sends no `start` part; use it only while the browser still holds that message. Before it answers 200,
|
|
43
|
+
* the helper reads once to check the token's Dex position; a position Dex rejects restarts the turn from
|
|
44
|
+
* its first retained event.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // Server: GET /api/chat/:id/stream
|
|
49
|
+
* export async function GET(request: Request, sessionId: string): Promise<Response> {
|
|
50
|
+
* await authorizeSession(request, sessionId);
|
|
51
|
+
* return createAgentUIMessageStreamResponse({
|
|
52
|
+
* client, flow, flowId: sessionId,
|
|
53
|
+
* resumeToken: request.headers.get(agentResumeTokenHeaderName) ?? undefined,
|
|
54
|
+
* signal: request.signal,
|
|
55
|
+
* });
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* // Browser
|
|
59
|
+
* let resumeToken: string | undefined = initial.resumeToken ?? undefined;
|
|
60
|
+
* const chat = useChat({
|
|
61
|
+
* id: sessionId,
|
|
62
|
+
* messages: initial.messages,
|
|
63
|
+
* resume: resumeToken !== undefined,
|
|
64
|
+
* onData: (part) => {
|
|
65
|
+
* if (part.type === "data-agent-resume-token") resumeToken = part.data as string;
|
|
66
|
+
* },
|
|
67
|
+
* transport: new DefaultChatTransport({
|
|
68
|
+
* api: "/api/chat",
|
|
69
|
+
* prepareReconnectToStreamRequest: ({ id }) => ({
|
|
70
|
+
* api: `/api/chat/${id}/stream`,
|
|
71
|
+
* headers: resumeToken === undefined ? {} : { "x-agent-resume-token": resumeToken },
|
|
72
|
+
* }),
|
|
73
|
+
* }),
|
|
74
|
+
* sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
* @param options - Session, start position, polling, and extra headers.
|
|
78
|
+
* @returns 200 with the stream; 204 when the token's turn already finished, or when the turn to replay is
|
|
79
|
+
* not active or not on the Stream; 400 with a text body when `resumeToken` is invalid.
|
|
80
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
81
|
+
* @throws {DexServiceError} When reading the start position fails.
|
|
82
|
+
*/
|
|
83
|
+
export async function createAgentUIMessageStreamResponse(options) {
|
|
84
|
+
const pollTimeoutMs = requireAgentEventPollTimeoutMs(options.pollTimeoutMs);
|
|
85
|
+
if (options.resumeToken !== undefined && decodeResumeTokenOrUndefined(options.resumeToken) === undefined) {
|
|
86
|
+
return createInvalidResumeTokenResponse(options.headers);
|
|
87
|
+
}
|
|
88
|
+
const start = await prepareAgentUIStreamStart(options);
|
|
89
|
+
if (start === undefined) {
|
|
90
|
+
return new Response(null, { status: 204, headers: mergeHeaders({}, options.headers) });
|
|
91
|
+
}
|
|
92
|
+
return createAgentUIMessageSseResponse(options, start, pollTimeoutMs, []);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Queues a user message and returns the AI SDK UI message stream Response of the turn it starts.
|
|
96
|
+
*
|
|
97
|
+
* Mount it as the send endpoint of `useChat`, `POST <api>` by default, when the last message in the
|
|
98
|
+
* request body is a user message. The chat ID in the body comes from the browser, so the route must check
|
|
99
|
+
* that the caller may write to that session. The helper reads the session's turn count and the Stream
|
|
100
|
+
* position first and then calls `send_user_message`, so the stream follows the next turn that starts
|
|
101
|
+
* after the message was queued. When an earlier turn is still running, the stream waits for it to end.
|
|
102
|
+
* When other messages were queued before this one, the stream follows the turn of the oldest of them.
|
|
103
|
+
*
|
|
104
|
+
* The session's committed state ends the stream even when no event of the turn reaches the Stream, such
|
|
105
|
+
* as with the Server's Stream Store disabled: once that turn ended, the stream finishes with reason
|
|
106
|
+
* `other`, and the application reloads the stored messages. The RPC needs a running Worker.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* // POST /api/chat
|
|
111
|
+
* export async function POST(request: Request): Promise<Response> {
|
|
112
|
+
* const { id, messages } = await request.json();
|
|
113
|
+
* await authorizeSession(request, id);
|
|
114
|
+
* const lastMessage = messages.at(-1);
|
|
115
|
+
* if (lastMessage.role === "assistant") {
|
|
116
|
+
* return resolveAgentToolApprovalsWithUIMessageStream({
|
|
117
|
+
* client, flow, flowId: id, approvals: readAgentToolApprovalResponses(lastMessage) });
|
|
118
|
+
* }
|
|
119
|
+
* const text = lastMessage.parts.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
120
|
+
* return sendAgentUserMessageWithUIMessageStream({ client, flow, flowId: id, text, signal: request.signal });
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
* @param options - Session, message text, polling, and extra headers.
|
|
124
|
+
* @returns 200 with the stream; 400 with a text body when the text is blank.
|
|
125
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
126
|
+
* @throws {FlowNotActiveOrNotFoundError} When the session is not running.
|
|
127
|
+
* @throws {DexServiceError} When a read or the RPC fails.
|
|
128
|
+
*/
|
|
129
|
+
export async function sendAgentUserMessageWithUIMessageStream(options) {
|
|
130
|
+
const pollTimeoutMs = requireAgentEventPollTimeoutMs(options.pollTimeoutMs);
|
|
131
|
+
const { client, flow, flowId } = options;
|
|
132
|
+
const [state] = await client.getAttributes(flowId, flow.agentState);
|
|
133
|
+
const eventResumeToken = await readAgentEventsTailResumeToken(client, flow, flowId);
|
|
134
|
+
const isQueued = await client.invokeRPC(flow.sendUserMessage, flowId, { text: options.text });
|
|
135
|
+
if (!isQueued) {
|
|
136
|
+
return new Response("the user message text is blank", {
|
|
137
|
+
status: 400,
|
|
138
|
+
headers: mergeHeaders({ "content-type": "text/plain; charset=utf-8" }, options.headers),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const cursor = createAgentUIStreamCursor(eventResumeToken, null, false);
|
|
142
|
+
// The message's turn starts after every turn that already started.
|
|
143
|
+
const activeTurnNumber = state?.activeTurnId == null ? 0 : (parseTurnNumber(state.activeTurnId) ?? 0);
|
|
144
|
+
cursor.afterTurnNumber = Math.max(state?.turnCount ?? 0, activeTurnNumber);
|
|
145
|
+
return createAgentUIMessageSseResponse(options, { cursor, firstMessage: undefined }, pollTimeoutMs, []);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sends one tool approval decision to the session's `resolve_interrupt` RPC.
|
|
149
|
+
*
|
|
150
|
+
* Call it with each AI SDK `tool-approval-response`: its `approvalId` is the `interrupt_id`, and its
|
|
151
|
+
* `reason` becomes the rejection message the model sees. It retries `RpcLockConflictError`, which a call
|
|
152
|
+
* that races the approval Step can raise, for up to 5 seconds. It resolves any pending interrupt of the
|
|
153
|
+
* session, so the route must check that the caller may decide for that session. The RPC needs a running
|
|
154
|
+
* Worker.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const isResolved = await resolveAgentToolApproval({
|
|
159
|
+
* client, flow, flowId: sessionId,
|
|
160
|
+
* approval: { approvalId: "call_42", approved: false, reason: "Use the sandbox account" },
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
* @param options - Session and decision.
|
|
164
|
+
* @returns `true` when the interrupt was pending and is now resolved; `false` when it is unknown, already
|
|
165
|
+
* resolved, or past its deadline.
|
|
166
|
+
* @throws {RpcLockConflictError} When the lock stays busy for 5 seconds.
|
|
167
|
+
* @throws {FlowNotActiveOrNotFoundError} When the session is not running.
|
|
168
|
+
*/
|
|
169
|
+
export async function resolveAgentToolApproval(options) {
|
|
170
|
+
const { approval } = options;
|
|
171
|
+
const deadline = Date.now() + lockConflictRetryTimeoutMs;
|
|
172
|
+
for (;;) {
|
|
173
|
+
try {
|
|
174
|
+
return await options.client.invokeRPC(options.flow.resolveInterrupt, options.flowId, {
|
|
175
|
+
interruptId: approval.approvalId,
|
|
176
|
+
approved: approval.approved,
|
|
177
|
+
rejectionMessage: approval.approved ? null : (approval.reason ?? null),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!(error instanceof RpcLockConflictError) || Date.now() >= deadline) {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
await delay(lockConflictRetryIntervalMs);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Returns the tool approval decisions recorded on an AI SDK assistant message.
|
|
190
|
+
*
|
|
191
|
+
* `useChat` sets a tool part to `approval-responded` when the application calls
|
|
192
|
+
* `addToolApprovalResponse`, then sends the message list with `sendAutomaticallyWhen`. Pass the last
|
|
193
|
+
* message of that request body. The message comes from the browser, so every field is checked; parts
|
|
194
|
+
* that are not well-formed responded approvals are skipped.
|
|
195
|
+
*
|
|
196
|
+
* Each decision is bound to the message's turn when the message ID is `<turnId>-assistant`, as the agent UI
|
|
197
|
+
* streams and snapshots name assistant messages; a message with another ID gives unbound decisions.
|
|
198
|
+
*
|
|
199
|
+
* @param message - An AI SDK `UIMessage`, typically the last message of a `useChat` request body.
|
|
200
|
+
* @returns One decision per `approval-responded` tool part, in part order; empty for anything else.
|
|
201
|
+
*/
|
|
202
|
+
export function readAgentToolApprovalResponses(message) {
|
|
203
|
+
if (!isRecord(message) || !Array.isArray(message["parts"])) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const messageId = message["id"];
|
|
207
|
+
const turnId = typeof messageId === "string" ? assistantMessageIdPattern.exec(messageId)?.[1] : undefined;
|
|
208
|
+
const approvals = [];
|
|
209
|
+
for (const part of message["parts"]) {
|
|
210
|
+
if (!isRecord(part) || part["state"] !== "approval-responded" || !isToolPartType(part["type"])) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const approval = part["approval"];
|
|
214
|
+
if (!isRecord(approval) || typeof approval["id"] !== "string" || typeof approval["approved"] !== "boolean") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const reason = approval["reason"];
|
|
218
|
+
approvals.push({
|
|
219
|
+
approvalId: approval["id"],
|
|
220
|
+
approved: approval["approved"],
|
|
221
|
+
...(typeof reason === "string" ? { reason } : {}),
|
|
222
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return approvals;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Sends tool approval decisions and returns the AI SDK UI message stream Response that continues the turn.
|
|
229
|
+
*
|
|
230
|
+
* Mount it in the send endpoint of `useChat` for requests whose last message is an assistant message,
|
|
231
|
+
* which `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses` sends after the
|
|
232
|
+
* application answered every approval. The decisions and the chat ID come from the browser, so the route
|
|
233
|
+
* must check that the caller may decide for that session. Only interrupts that are pending in the
|
|
234
|
+
* session's active turn are resolved.
|
|
235
|
+
*
|
|
236
|
+
* The stream continues the same assistant message. A decision that is no longer pending, because another
|
|
237
|
+
* client or a deadline settled the call or its turn ended, gets the call's known outcome, or an error
|
|
238
|
+
* part when none is known, so the browser leaves `approval-responded` and does not send the decision
|
|
239
|
+
* again. Decisions bound to another turn than the active one, such as from a stale browser tab, are all
|
|
240
|
+
* stale, and the stream then ends after their outcomes. With `resumeToken`, the latest `data-agent-resume-token`, the stream continues exactly after the
|
|
241
|
+
* paused stream. Without one it reads the Stream position and the active turn's tool calls before
|
|
242
|
+
* sending the decisions. The RPCs need a running Worker.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* return resolveAgentToolApprovalsWithUIMessageStream({
|
|
247
|
+
* client, flow, flowId: id, approvals: readAgentToolApprovalResponses(messages.at(-1)) });
|
|
248
|
+
* ```
|
|
249
|
+
* @param options - Session, decisions, optional resume token, polling, and extra headers.
|
|
250
|
+
* @returns 200 with the stream, which holds only the stale decisions' outcomes and a `finish` part when
|
|
251
|
+
* no turn is active; 400 with a text body when `resumeToken` is invalid.
|
|
252
|
+
* @throws {RangeError} When `pollTimeoutMs` is not a positive whole number of seconds.
|
|
253
|
+
* @throws {RpcLockConflictError} When a decision's lock stays busy for 5 seconds.
|
|
254
|
+
* @throws {DexServiceError} When a read or an RPC fails.
|
|
255
|
+
*/
|
|
256
|
+
export async function resolveAgentToolApprovalsWithUIMessageStream(options) {
|
|
257
|
+
const pollTimeoutMs = requireAgentEventPollTimeoutMs(options.pollTimeoutMs);
|
|
258
|
+
const { client, flow, flowId } = options;
|
|
259
|
+
let clientCursor;
|
|
260
|
+
if (options.resumeToken !== undefined) {
|
|
261
|
+
clientCursor = decodeResumeTokenOrUndefined(options.resumeToken);
|
|
262
|
+
if (clientCursor === undefined) {
|
|
263
|
+
return createInvalidResumeTokenResponse(options.headers);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Position before snapshot: events may repeat the snapshot, but none goes missing.
|
|
267
|
+
const tailResumeToken = await readAgentEventsTailResumeToken(client, flow, flowId);
|
|
268
|
+
const snapshot = await client.invokeRPC(flow.getAgentSnapshot, flowId);
|
|
269
|
+
const activeTurnId = snapshot.state?.activeTurnId ?? null;
|
|
270
|
+
// Decisions from an older turn's message settle that message only; the active turn goes elsewhere.
|
|
271
|
+
const isActiveTurnRequest = options.approvals.every((approval) => approval.turnId === undefined || approval.turnId === activeTurnId);
|
|
272
|
+
let start;
|
|
273
|
+
if (!isActiveTurnRequest) {
|
|
274
|
+
const finishedCursor = createAgentUIStreamCursor(tailResumeToken, null, true);
|
|
275
|
+
finishedCursor.isTurnFinished = true;
|
|
276
|
+
start = { cursor: finishedCursor, firstMessage: undefined };
|
|
277
|
+
}
|
|
278
|
+
else if (clientCursor !== undefined && clientCursor.turnId === activeTurnId && activeTurnId !== null) {
|
|
279
|
+
// The browser keeps its message here, so a rejected position falls back to the tail, never a replay.
|
|
280
|
+
const probe = await probeAgentEventResumeToken(client, flow, flowId, clientCursor.eventResumeToken);
|
|
281
|
+
if (probe.isAccepted) {
|
|
282
|
+
start = { cursor: clientCursor, firstMessage: probe.firstMessage };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
start ??= {
|
|
286
|
+
cursor: await createActiveTurnContinuationCursor(client, flow, flowId, tailResumeToken, activeTurnId, snapshot.toolCalls),
|
|
287
|
+
firstMessage: undefined,
|
|
288
|
+
};
|
|
289
|
+
const cursor = start.cursor;
|
|
290
|
+
const pendingInterruptIds = new Set(snapshot.pendingInterrupts
|
|
291
|
+
.filter((interrupt) => snapshot.toolCalls.some((toolCall) => toolCall.toolCallId === interrupt.toolCallId &&
|
|
292
|
+
toolCall.turnId === activeTurnId &&
|
|
293
|
+
toolCall.status === "awaiting_approval"))
|
|
294
|
+
.map((interrupt) => interrupt.interruptId));
|
|
295
|
+
const staleApprovals = [];
|
|
296
|
+
for (const approval of selectUniqueApprovals(options.approvals)) {
|
|
297
|
+
const isResolved = isActiveTurnRequest &&
|
|
298
|
+
pendingInterruptIds.has(approval.approvalId) &&
|
|
299
|
+
(await resolveAgentToolApproval({ client, flow, flowId, approval }));
|
|
300
|
+
if (isResolved) {
|
|
301
|
+
// The interrupt_resolved event follows; until then the call must not count as waiting.
|
|
302
|
+
cursor.toolCalls.set(approval.approvalId, "approval_resolved");
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
staleApprovals.push(approval);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const staleOutcomeChunks = await readStaleApprovalOutcomes(new AgentSessionRecordReader(client, flow, flowId), activeTurnId, snapshot.toolCalls, staleApprovals, cursor);
|
|
309
|
+
return createAgentUIMessageSseResponse(options, start, pollTimeoutMs, staleOutcomeChunks);
|
|
310
|
+
}
|
|
311
|
+
/** Keeps the first well-formed decision per approval ID, up to the per-request limit. */
|
|
312
|
+
function selectUniqueApprovals(approvals) {
|
|
313
|
+
const selected = new Map();
|
|
314
|
+
for (const approval of approvals.slice(0, maximumApprovalsPerRequest)) {
|
|
315
|
+
const approvalId = approval.approvalId;
|
|
316
|
+
if (typeof approvalId !== "string" ||
|
|
317
|
+
approvalId === "" ||
|
|
318
|
+
approvalId.length > maximumToolCallIdLength ||
|
|
319
|
+
approvalId.includes("/") ||
|
|
320
|
+
typeof approval.approved !== "boolean" ||
|
|
321
|
+
selected.has(approvalId)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
selected.set(approvalId, approval);
|
|
325
|
+
}
|
|
326
|
+
return [...selected.values()];
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Returns the parts that settle the decisions `resolve_interrupt` did not accept, and records them in the
|
|
330
|
+
* cursor. A call still open in the active turn gets none: its own events follow.
|
|
331
|
+
*/
|
|
332
|
+
async function readStaleApprovalOutcomes(records, activeTurnId, activeTurnToolCalls, staleApprovals, cursor) {
|
|
333
|
+
const chunks = [];
|
|
334
|
+
let storedToolResults;
|
|
335
|
+
for (const approval of staleApprovals) {
|
|
336
|
+
// A tool approval's interrupt ID is its tool call ID.
|
|
337
|
+
const toolCallId = approval.approvalId;
|
|
338
|
+
const isActiveTurnToolCall = cursor.turnId === activeTurnId && activeTurnToolCalls.some((toolCall) => toolCall.toolCallId === toolCallId);
|
|
339
|
+
const record = activeTurnId !== null && isActiveTurnToolCall
|
|
340
|
+
? await records.readToolCall(activeTurnId, toolCallId)
|
|
341
|
+
: undefined;
|
|
342
|
+
if (record !== undefined && !isFinalToolCallStatus(record.status)) {
|
|
343
|
+
cursor.toolCalls.set(toolCallId, "approval_resolved");
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
cursor.toolCalls.set(toolCallId, "finished");
|
|
347
|
+
if (record !== undefined) {
|
|
348
|
+
if (record.status === "succeeded") {
|
|
349
|
+
chunks.push({ type: "tool-output-available", toolCallId, output: record.output ?? null, dynamic: true });
|
|
350
|
+
}
|
|
351
|
+
else if (record.status === "rejected") {
|
|
352
|
+
chunks.push({ type: "tool-output-denied", toolCallId });
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
chunks.push({ type: "tool-output-error", toolCallId, errorText: describeToolCallOutcome(record), dynamic: true });
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
storedToolResults ??= await records.readRecentStoredToolResults();
|
|
360
|
+
const storedToolOutput = [...storedToolResults]
|
|
361
|
+
.reverse()
|
|
362
|
+
.find((result) => result.toolCallId === toolCallId && (approval.turnId === undefined || result.turnId === approval.turnId))?.toolOutput;
|
|
363
|
+
chunks.push(storedToolOutput === undefined
|
|
364
|
+
? { type: "tool-output-error", toolCallId, errorText: staleApprovalText, dynamic: true }
|
|
365
|
+
: { type: "tool-output-available", toolCallId, output: storedToolOutput, dynamic: true });
|
|
366
|
+
}
|
|
367
|
+
return chunks;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Returns a cursor at the Stream tail that continues the active turn the browser already shows. It records
|
|
371
|
+
* where the turn started, so a reconnect with its token replays the whole turn.
|
|
372
|
+
*/
|
|
373
|
+
async function createActiveTurnContinuationCursor(client, flow, flowId, tailResumeToken, activeTurnId, toolCalls) {
|
|
374
|
+
const cursor = createAgentUIStreamCursor(tailResumeToken, activeTurnId, true);
|
|
375
|
+
if (activeTurnId === null) {
|
|
376
|
+
cursor.isTurnFinished = true;
|
|
377
|
+
return cursor;
|
|
378
|
+
}
|
|
379
|
+
cursor.turnStartResumeToken =
|
|
380
|
+
(await findAgentEventResumeTokenBeforeTurn(client, flow, flowId, activeTurnId)) ?? null;
|
|
381
|
+
for (const toolCall of toolCalls) {
|
|
382
|
+
cursor.toolCalls.set(toolCall.toolCallId, toolCallUIStateOfRecord(toolCall));
|
|
383
|
+
}
|
|
384
|
+
return cursor;
|
|
385
|
+
}
|
|
386
|
+
function createAgentUIMessageSseResponse(options, start, pollTimeoutMs, leadingChunks) {
|
|
387
|
+
return createServerSentEventResponse(formatAgentUIMessageSseFrames(options, start, pollTimeoutMs, leadingChunks), agentUIMessageStreamHeaders, options.headers);
|
|
388
|
+
}
|
|
389
|
+
async function* formatAgentUIMessageSseFrames(options, start, pollTimeoutMs, leadingChunks) {
|
|
390
|
+
const cursor = start.cursor;
|
|
391
|
+
if (leadingChunks.length > 0) {
|
|
392
|
+
const resumeTokenChunk = {
|
|
393
|
+
type: agentResumeTokenDataPartType,
|
|
394
|
+
data: encodeAgentUIStreamCursor(cursor),
|
|
395
|
+
transient: true,
|
|
396
|
+
};
|
|
397
|
+
yield [...leadingChunks, resumeTokenChunk].map(formatUIMessageChunkFrame).join("");
|
|
398
|
+
}
|
|
399
|
+
if (cursor.isTurnFinished) {
|
|
400
|
+
yield formatUIMessageChunkFrame({ type: "finish", finishReason: "other" });
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
for await (const chunks of readAgentUIMessageChunkBatches(options, start, pollTimeoutMs)) {
|
|
404
|
+
if (chunks === undefined) {
|
|
405
|
+
yield serverSentEventKeepAliveFrame;
|
|
406
|
+
}
|
|
407
|
+
else if (chunks.length > 0) {
|
|
408
|
+
yield chunks.map(formatUIMessageChunkFrame).join("");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
yield "data: [DONE]\n\n";
|
|
413
|
+
}
|
|
414
|
+
function formatUIMessageChunkFrame(chunk) {
|
|
415
|
+
return `data: ${JSON.stringify(chunk)}\n\n`;
|
|
416
|
+
}
|
|
417
|
+
function decodeResumeTokenOrUndefined(resumeToken) {
|
|
418
|
+
try {
|
|
419
|
+
return decodeAgentUIStreamCursor(resumeToken);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (error instanceof TypeError) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function createInvalidResumeTokenResponse(headers) {
|
|
429
|
+
return new Response("the agent UI stream resume token is invalid", {
|
|
430
|
+
status: 400,
|
|
431
|
+
headers: mergeHeaders({ "content-type": "text/plain; charset=utf-8" }, headers),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function isToolPartType(type) {
|
|
435
|
+
return type === "dynamic-tool" || (typeof type === "string" && type.startsWith("tool-"));
|
|
436
|
+
}
|
|
437
|
+
function isRecord(value) {
|
|
438
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
439
|
+
}
|
|
440
|
+
//# sourceMappingURL=agent-ui-message-stream-responses.js.map
|