@gea-ai/agent-sdk 0.1.261009-alpha.0 → 0.1.261010-alpha.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/dist/agent-call.d.ts +3 -1
- package/dist/agent-call.js +65 -5
- package/dist/agent-channel-api.js +3 -3
- package/dist/agent-remote-run.js +35 -6
- package/dist/agent-run-host.d.ts +4 -0
- package/dist/agent-run-host.js +95 -11
- package/dist/agent-session-children.js +35 -9
- package/dist/agent-session-inbox.js +63 -6
- package/dist/agent-worker.js +116 -32
- package/dist/gea-ai.js +2 -1
- package/dist/host-rejection.d.ts +12 -0
- package/dist/host-rejection.js +78 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/agent-call.d.ts
CHANGED
|
@@ -1225,7 +1225,9 @@ export declare function createWorkerRunCancelTool({ host, }: {
|
|
|
1225
1225
|
status: "abort_requested" | "not_running";
|
|
1226
1226
|
}, NoInfer<import("@ai-sdk/provider-utils").Context>>;
|
|
1227
1227
|
});
|
|
1228
|
-
export declare function readWorkerAgentResponse(response: Response
|
|
1228
|
+
export declare function readWorkerAgentResponse(response: Response, options?: {
|
|
1229
|
+
persistedSessionWaiting?: () => Promise<boolean>;
|
|
1230
|
+
}): Promise<{
|
|
1229
1231
|
status: "completed" | "suspended";
|
|
1230
1232
|
text: string;
|
|
1231
1233
|
}>;
|
package/dist/agent-call.js
CHANGED
|
@@ -70,6 +70,23 @@ export function createWorkerAgentTool(input) {
|
|
|
70
70
|
error: "child_busy",
|
|
71
71
|
message: "This child Session has unfinished work. Wait for its Run before continuing the Session.",
|
|
72
72
|
};
|
|
73
|
+
if (response.status === 403) {
|
|
74
|
+
const rejection = z
|
|
75
|
+
.object({
|
|
76
|
+
error: z
|
|
77
|
+
.string()
|
|
78
|
+
.max(80)
|
|
79
|
+
.regex(/^[A-Za-z0-9_.-]+$/),
|
|
80
|
+
})
|
|
81
|
+
.safeParse(value);
|
|
82
|
+
if (rejection.success) {
|
|
83
|
+
console.error("Child run rejected", {
|
|
84
|
+
status: 403,
|
|
85
|
+
error: rejection.data.error,
|
|
86
|
+
});
|
|
87
|
+
throw new Error(`Agent invocation was rejected (403 ${rejection.data.error}).`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
73
90
|
}
|
|
74
91
|
throw new Error(`Agent invocation was rejected (${response.status}).`);
|
|
75
92
|
}
|
|
@@ -99,7 +116,15 @@ export function createWorkerRunCancelTool({ host, }) {
|
|
|
99
116
|
}
|
|
100
117
|
class AgentChildRunResponseError extends TaggedError("AgentChildRunResponseError")() {
|
|
101
118
|
}
|
|
102
|
-
|
|
119
|
+
const childDiagnosticIdentifierPattern = /^[A-Za-z0-9_.-]+$/;
|
|
120
|
+
function childDiagnosticIdentifier(value) {
|
|
121
|
+
return typeof value === "string" &&
|
|
122
|
+
value.length <= 80 &&
|
|
123
|
+
childDiagnosticIdentifierPattern.test(value)
|
|
124
|
+
? value
|
|
125
|
+
: undefined;
|
|
126
|
+
}
|
|
127
|
+
export async function readWorkerAgentResponse(response, options) {
|
|
103
128
|
if (response.status === 409 &&
|
|
104
129
|
response.headers.get("x-gea-session-execution") === "already-started") {
|
|
105
130
|
await response.body?.cancel();
|
|
@@ -117,6 +142,9 @@ export async function readWorkerAgentResponse(response) {
|
|
|
117
142
|
let completed = false;
|
|
118
143
|
let suspended = false;
|
|
119
144
|
let failure;
|
|
145
|
+
let legacyToolCalls = false;
|
|
146
|
+
let otherUnacceptableFinish = false;
|
|
147
|
+
let sawStreamFailure = false;
|
|
120
148
|
const reader = parseJsonEventStream({
|
|
121
149
|
stream: response.body,
|
|
122
150
|
schema: uiMessageChunkSchema,
|
|
@@ -133,26 +161,46 @@ export async function readWorkerAgentResponse(response) {
|
|
|
133
161
|
const chunk = event.value;
|
|
134
162
|
if (chunk.type === "text-delta")
|
|
135
163
|
text += chunk.delta;
|
|
136
|
-
if (chunk.type === "error")
|
|
164
|
+
if (chunk.type === "error") {
|
|
165
|
+
sawStreamFailure = true;
|
|
137
166
|
failure ??= new AgentChildRunResponseError({
|
|
138
167
|
code: "child_stream_error",
|
|
139
168
|
message: chunk.errorText,
|
|
140
169
|
});
|
|
141
|
-
|
|
170
|
+
}
|
|
171
|
+
if (chunk.type === "abort") {
|
|
172
|
+
sawStreamFailure = true;
|
|
142
173
|
failure ??= new AgentChildRunResponseError({
|
|
143
174
|
code: "child_stream_aborted",
|
|
144
175
|
message: "Child Agent was cancelled.",
|
|
145
176
|
});
|
|
177
|
+
}
|
|
146
178
|
if (chunk.type === "finish") {
|
|
147
179
|
suspended = z
|
|
148
180
|
.object({ sessionStatus: z.literal("waiting") })
|
|
149
181
|
.safeParse(chunk.messageMetadata).success;
|
|
150
|
-
|
|
151
|
-
!(suspended && chunk.finishReason === "tool-calls")
|
|
182
|
+
const unacceptable = chunk.finishReason !== "stop" &&
|
|
183
|
+
!(suspended && chunk.finishReason === "tool-calls");
|
|
184
|
+
if (unacceptable) {
|
|
185
|
+
if (chunk.finishReason === "tool-calls" && !suspended)
|
|
186
|
+
legacyToolCalls = true;
|
|
187
|
+
else
|
|
188
|
+
otherUnacceptableFinish = true;
|
|
189
|
+
const parsedSession = z
|
|
190
|
+
.object({ sessionStatus: z.unknown() })
|
|
191
|
+
.safeParse(chunk.messageMetadata);
|
|
192
|
+
const rawSessionStatus = parsedSession.success
|
|
193
|
+
? parsedSession.data.sessionStatus
|
|
194
|
+
: undefined;
|
|
195
|
+
const sessionStatus = childDiagnosticIdentifier(rawSessionStatus);
|
|
152
196
|
failure ??= new AgentChildRunResponseError({
|
|
153
197
|
code: "child_finish_reason",
|
|
154
198
|
message: `Child Agent did not complete (${chunk.finishReason ?? "unknown"}).`,
|
|
199
|
+
finishReason: childDiagnosticIdentifier(chunk.finishReason) ?? "unknown",
|
|
200
|
+
hasSessionStatus: rawSessionStatus !== undefined,
|
|
201
|
+
...(sessionStatus ? { sessionStatus } : {}),
|
|
155
202
|
});
|
|
203
|
+
}
|
|
156
204
|
completed = true;
|
|
157
205
|
}
|
|
158
206
|
}
|
|
@@ -161,6 +209,18 @@ export async function readWorkerAgentResponse(response) {
|
|
|
161
209
|
await reader.cancel();
|
|
162
210
|
reader.releaseLock();
|
|
163
211
|
}
|
|
212
|
+
if (options?.persistedSessionWaiting &&
|
|
213
|
+
legacyToolCalls &&
|
|
214
|
+
!otherUnacceptableFinish &&
|
|
215
|
+
!sawStreamFailure &&
|
|
216
|
+
failure instanceof AgentChildRunResponseError &&
|
|
217
|
+
failure.code === "child_finish_reason") {
|
|
218
|
+
const waiting = await Result.tryPromise(() => options.persistedSessionWaiting());
|
|
219
|
+
if (waiting.isOk() && waiting.value) {
|
|
220
|
+
failure = undefined;
|
|
221
|
+
suspended = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
164
224
|
if (failure)
|
|
165
225
|
throw failure;
|
|
166
226
|
if (!completed)
|
|
@@ -40,7 +40,7 @@ export function extractChannelReply(messageValue, responseMessageId) {
|
|
|
40
40
|
}
|
|
41
41
|
/** Channel owns delivery; ordinary Agents API owns admission, execution and history. */
|
|
42
42
|
export async function executeChannelAgentRun(input) {
|
|
43
|
-
const fetchApi = async (path, body) => {
|
|
43
|
+
const fetchApi = async (path, body, headers) => {
|
|
44
44
|
input.signal.throwIfAborted();
|
|
45
45
|
const response = await input.binding.fetch(`/api/v1${path}`, {
|
|
46
46
|
method: body === undefined ? "GET" : "POST",
|
|
@@ -48,7 +48,7 @@ export async function executeChannelAgentRun(input) {
|
|
|
48
48
|
...(body === undefined
|
|
49
49
|
? {}
|
|
50
50
|
: {
|
|
51
|
-
headers: { "content-type": "application/json" },
|
|
51
|
+
headers: { "content-type": "application/json", ...headers },
|
|
52
52
|
body: JSON.stringify(body),
|
|
53
53
|
}),
|
|
54
54
|
});
|
|
@@ -67,7 +67,7 @@ export async function executeChannelAgentRun(input) {
|
|
|
67
67
|
id: input.runId,
|
|
68
68
|
input: input.message,
|
|
69
69
|
stream: false,
|
|
70
|
-
})).body?.cancel();
|
|
70
|
+
}, { "idempotency-key": input.runId })).body?.cancel();
|
|
71
71
|
let stream = true;
|
|
72
72
|
for (;;) {
|
|
73
73
|
const current = runSchema.parse(await (await fetchApi(`/runs/${input.runId}`)).json());
|
package/dist/agent-remote-run.js
CHANGED
|
@@ -62,13 +62,42 @@ export async function submitRemoteRun(fetch, input, options = {}) {
|
|
|
62
62
|
}))).id;
|
|
63
63
|
signal.throwIfAborted();
|
|
64
64
|
submitting = true;
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// One key per logical submission. Retries reuse it so a lost 202 cannot
|
|
66
|
+
// start a second Run. A later call to submitRemoteRun is a new request.
|
|
67
|
+
const idempotencyKey = crypto.randomUUID();
|
|
68
|
+
const runBody = JSON.stringify({
|
|
69
|
+
id: idempotencyKey,
|
|
70
|
+
input: message,
|
|
71
|
+
stream: false,
|
|
71
72
|
});
|
|
73
|
+
const runHeaders = {
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
"idempotency-key": idempotencyKey,
|
|
76
|
+
};
|
|
77
|
+
let response;
|
|
78
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
try {
|
|
81
|
+
response = await fetch(`/sessions/${sessionId}/runs`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: runHeaders,
|
|
84
|
+
signal,
|
|
85
|
+
body: runBody,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (signal.aborted || attempt === 2)
|
|
90
|
+
throw error;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const retryable = [502, 503, 504].includes(response.status);
|
|
94
|
+
const admitted = response.headers.has("x-gea-agent-run-id");
|
|
95
|
+
if (!retryable || admitted || attempt === 2)
|
|
96
|
+
break;
|
|
97
|
+
await response.body?.cancel();
|
|
98
|
+
}
|
|
99
|
+
if (!response)
|
|
100
|
+
throw new Error("Remote Run was not submitted.");
|
|
72
101
|
rejected = [400, 401, 403, 404, 405, 409, 413, 415, 422, 429].includes(response.status);
|
|
73
102
|
const run = agentHttpRunSchema.parse(await responseJson(response));
|
|
74
103
|
if (run.session_id !== sessionId)
|
package/dist/agent-run-host.d.ts
CHANGED
|
@@ -11,17 +11,21 @@ export type AgentChildRunSession = (operation: string, body: unknown, traceConte
|
|
|
11
11
|
export declare class AgentRunHost {
|
|
12
12
|
private readonly host;
|
|
13
13
|
private readonly executions;
|
|
14
|
+
private readonly executionDone;
|
|
14
15
|
private readonly work;
|
|
15
16
|
private closed;
|
|
16
17
|
private readonly shutdown;
|
|
17
18
|
constructor(host: {
|
|
18
19
|
session: AgentChildRunSession;
|
|
20
|
+
/** Authorize against the root Session and return its retained child binding. */
|
|
21
|
+
resolveSession?(sessionId: string): Promise<Response>;
|
|
19
22
|
remoteFetch?(destination: RemoteRunDestination, path: string, init?: RequestInit): Promise<Response>;
|
|
20
23
|
transferRemoteInputs?(destination: RemoteRunDestination, files: RemoteInputFiles, scope: StudioArtifactScope | undefined, signal: AbortSignal): Promise<RemoteFileMapping>;
|
|
21
24
|
register(run: AgentChildRun): Promise<string>;
|
|
22
25
|
invoke(run: AgentChildRun, signal: AbortSignal, continuation?: TraceContinuation, inboxExecution?: AgentSessionInboxExecution): Promise<{
|
|
23
26
|
response: Response;
|
|
24
27
|
dispose?: () => void;
|
|
28
|
+
persistedSessionWaiting?: () => Promise<boolean>;
|
|
25
29
|
}>;
|
|
26
30
|
cancelChild(run: AgentChildRun): Promise<boolean | void>;
|
|
27
31
|
onError(error: unknown, identity?: {
|
package/dist/agent-run-host.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { submitRemoteRun, observeRemoteRun, cancelRemoteRun, } from "./agent-remote-run.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { Result } from "better-result";
|
|
4
|
-
import { agentChildRunSchema, getAgentCallFileIds, agentChildRunListResultSchema, agentChildRunStartResultSchema, } from "@gea-ai/contract/agent-worker-tools";
|
|
4
|
+
import { agentChildRunSchema, agentChildSessionSchema, getAgentCallFileIds, agentChildRunListResultSchema, agentChildRunStartResultSchema, } from "@gea-ai/contract/agent-worker-tools";
|
|
5
5
|
import { readWorkerAgentResponse } from "./agent-call.js";
|
|
6
6
|
export { readWorkerAgentResponse } from "./agent-call.js";
|
|
7
7
|
export { streamManagedAgentSessionRun } from "./agent-session-stream.js";
|
|
@@ -9,6 +9,7 @@ export { streamManagedAgentSessionRun } from "./agent-session-stream.js";
|
|
|
9
9
|
export class AgentRunHost {
|
|
10
10
|
host;
|
|
11
11
|
executions = new Map();
|
|
12
|
+
executionDone = new Map();
|
|
12
13
|
work = new Set();
|
|
13
14
|
closed = false;
|
|
14
15
|
shutdown = new AbortController();
|
|
@@ -31,24 +32,94 @@ export class AgentRunHost {
|
|
|
31
32
|
return this.host.session(operation, body, traceContext);
|
|
32
33
|
}
|
|
33
34
|
async start(input) {
|
|
34
|
-
|
|
35
|
+
let authorizedSession;
|
|
36
|
+
if (input.sessionId && this.host.resolveSession) {
|
|
37
|
+
const response = await this.host.resolveSession(input.sessionId);
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
return response;
|
|
40
|
+
authorizedSession = z
|
|
41
|
+
.object({ session: agentChildSessionSchema })
|
|
42
|
+
.parse(await response.json()).session;
|
|
43
|
+
if (authorizedSession.sessionId !== input.sessionId ||
|
|
44
|
+
(input.target !== undefined &&
|
|
45
|
+
input.target !==
|
|
46
|
+
(authorizedSession.target ?? authorizedSession.agentKey)))
|
|
47
|
+
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
48
|
+
input = {
|
|
49
|
+
...input,
|
|
50
|
+
agentKey: authorizedSession.agentKey,
|
|
51
|
+
target: authorizedSession.target ?? authorizedSession.agentKey,
|
|
52
|
+
destination: authorizedSession.destination,
|
|
53
|
+
};
|
|
54
|
+
// Existing parent ledgers already carry this immutable binding. Keep their
|
|
55
|
+
// original start payload so deployed strict-schema bundles still resume.
|
|
56
|
+
const listed = await this.host.session("list", {});
|
|
57
|
+
if (!listed.ok)
|
|
58
|
+
return listed;
|
|
59
|
+
const page = agentChildRunListResultSchema.parse(await listed.json());
|
|
60
|
+
if (page.runs.some((run) => run.sessionId === input.sessionId)) {
|
|
61
|
+
authorizedSession = undefined;
|
|
62
|
+
}
|
|
63
|
+
else if (page.subagentConcurrencyVersion) {
|
|
64
|
+
const local = await this.host.session("session", {
|
|
65
|
+
sessionId: input.sessionId,
|
|
66
|
+
});
|
|
67
|
+
if (local.ok) {
|
|
68
|
+
z.object({
|
|
69
|
+
session: agentChildSessionSchema.omit({ sessionId: true }),
|
|
70
|
+
}).parse(await local.json());
|
|
71
|
+
authorizedSession = undefined;
|
|
72
|
+
}
|
|
73
|
+
else if (local.status !== 403)
|
|
74
|
+
return local;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (input.sessionId) {
|
|
35
78
|
const response = await this.host.session("list", {});
|
|
36
79
|
if (!response.ok)
|
|
37
80
|
return response;
|
|
38
81
|
const page = agentChildRunListResultSchema.parse(await response.json());
|
|
39
|
-
|
|
82
|
+
const listed = page.runs.find((run) => run.sessionId === input.sessionId);
|
|
83
|
+
let destination = listed?.destination;
|
|
84
|
+
let recordedTarget;
|
|
40
85
|
if (page.subagentConcurrencyVersion) {
|
|
41
86
|
const relation = await this.host.session("session", {
|
|
42
87
|
sessionId: input.sessionId,
|
|
43
88
|
});
|
|
89
|
+
// This parent ledger does not own the Session. Do not invent a target.
|
|
44
90
|
if (!relation.ok)
|
|
45
91
|
return relation;
|
|
46
|
-
|
|
92
|
+
const session = z
|
|
47
93
|
.object({
|
|
48
|
-
session: agentChildRunSchema
|
|
94
|
+
session: agentChildRunSchema
|
|
95
|
+
.pick({ target: true, agentKey: true, destination: true })
|
|
96
|
+
.strip(),
|
|
49
97
|
})
|
|
50
|
-
.parse(await relation.json()).session
|
|
98
|
+
.parse(await relation.json()).session;
|
|
99
|
+
destination = session.destination;
|
|
100
|
+
recordedTarget = session.target ?? session.agentKey;
|
|
51
101
|
}
|
|
102
|
+
else if (!listed) {
|
|
103
|
+
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
recordedTarget = listed.target ?? listed.agentKey;
|
|
107
|
+
}
|
|
108
|
+
// v0.56.2 bundles admit resume only when
|
|
109
|
+
// (stored target ?? agentKey) === (request target ?? request agentKey).
|
|
110
|
+
// Dispatch sends the parent agentKey and may omit target. Copy the
|
|
111
|
+
// ledger target after ownership is confirmed. Legacy remote admission
|
|
112
|
+
// compares the stored target on this host and does not use that copy.
|
|
113
|
+
const callsBundle = destination?.type !== "remote" || !!page.subagentConcurrencyVersion;
|
|
114
|
+
if (callsBundle &&
|
|
115
|
+
recordedTarget !== undefined &&
|
|
116
|
+
input.target !== undefined &&
|
|
117
|
+
input.target !== recordedTarget)
|
|
118
|
+
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
119
|
+
if (callsBundle &&
|
|
120
|
+
input.target === undefined &&
|
|
121
|
+
recordedTarget !== undefined)
|
|
122
|
+
input = { ...input, target: recordedTarget };
|
|
52
123
|
if (destination?.type === "remote")
|
|
53
124
|
return this.startRemote({ ...input, destination });
|
|
54
125
|
input = { ...input, destination: destination ?? input.destination };
|
|
@@ -56,7 +127,10 @@ export class AgentRunHost {
|
|
|
56
127
|
if (this.closed)
|
|
57
128
|
return new Response("Run dispatcher is stopping", { status: 503 });
|
|
58
129
|
const { artifactScope: _artifactScope, signal: _signal, ...sessionInput } = input;
|
|
59
|
-
const response = await this.host.session("start",
|
|
130
|
+
const response = await this.host.session("start", {
|
|
131
|
+
...sessionInput,
|
|
132
|
+
...(authorizedSession ? { authorizedSession } : {}),
|
|
133
|
+
});
|
|
60
134
|
if (!response.ok)
|
|
61
135
|
return response;
|
|
62
136
|
const { run, scheduled } = agentChildRunStartResultSchema.parse(await response.json());
|
|
@@ -96,10 +170,12 @@ export class AgentRunHost {
|
|
|
96
170
|
if (input.sessionId) {
|
|
97
171
|
const previous = runs.find((run) => run.sessionId === input.sessionId);
|
|
98
172
|
if (!previous ||
|
|
99
|
-
|
|
173
|
+
(input.target !== undefined && input.target !== previous.target) ||
|
|
100
174
|
JSON.stringify(previous.destination) !==
|
|
101
175
|
JSON.stringify(input.destination))
|
|
102
176
|
return new Response("Unknown child Session", { status: 403 });
|
|
177
|
+
if (input.target === undefined && previous.target !== undefined)
|
|
178
|
+
sessionInput.target = previous.target;
|
|
103
179
|
if (runs.some((run) => run.sessionId === input.sessionId && !run.outcome))
|
|
104
180
|
return new Response("Child Session is busy", { status: 409 });
|
|
105
181
|
}
|
|
@@ -185,7 +261,11 @@ export class AgentRunHost {
|
|
|
185
261
|
runId: run.id,
|
|
186
262
|
sessionId: run.sessionId,
|
|
187
263
|
});
|
|
188
|
-
this.
|
|
264
|
+
const done = this.execute(run, cancellation, inboxExecution).finally(() => {
|
|
265
|
+
this.executionDone.delete(run.id);
|
|
266
|
+
});
|
|
267
|
+
this.executionDone.set(run.id, done);
|
|
268
|
+
this.track(done, {
|
|
189
269
|
runId: run.id,
|
|
190
270
|
sessionId: run.sessionId,
|
|
191
271
|
});
|
|
@@ -268,7 +348,9 @@ export class AgentRunHost {
|
|
|
268
348
|
await this.host.register(run);
|
|
269
349
|
const invocation = await this.host.invoke(run, cancellation.signal, inboxExecution?.tracing ?? { parent: run.traceContext, links: [] }, inboxExecution);
|
|
270
350
|
try {
|
|
271
|
-
await readWorkerAgentResponse(invocation.response
|
|
351
|
+
await readWorkerAgentResponse(invocation.response, {
|
|
352
|
+
persistedSessionWaiting: invocation.persistedSessionWaiting,
|
|
353
|
+
});
|
|
272
354
|
}
|
|
273
355
|
finally {
|
|
274
356
|
invocation.dispose?.();
|
|
@@ -308,8 +390,10 @@ export class AgentRunHost {
|
|
|
308
390
|
}
|
|
309
391
|
else {
|
|
310
392
|
this.dispatch(run);
|
|
311
|
-
if ((await this.host.cancelChild(run)) !== false)
|
|
393
|
+
if ((await this.host.cancelChild(run)) !== false) {
|
|
312
394
|
this.executions.get(runId)?.abort();
|
|
395
|
+
await this.executionDone.get(runId);
|
|
396
|
+
}
|
|
313
397
|
}
|
|
314
398
|
}
|
|
315
399
|
return Response.json({
|
|
@@ -3,7 +3,7 @@ import { agentApplicationAssistantMessageSchema } from "@gea-ai/contract/agent-a
|
|
|
3
3
|
import { readTraceContext } from "./trace-context.js";
|
|
4
4
|
import { traceContextSchema } from "@gea-ai/contract/traces";
|
|
5
5
|
import { DEFAULT_MAX_CONCURRENT_SUBAGENTS, maxConcurrentSubagentsSchema, } from "@gea-ai/contract/agent-packages";
|
|
6
|
-
import { AGENT_SUBAGENT_CONCURRENCY_HEADER, agentChildRunSchema, agentChildRunStartInputSchema, agentChildRunDestinationSchema, agentChildRunOutcomeSchema, agentChildRunListInputSchema, } from "@gea-ai/contract/agent-worker-tools";
|
|
6
|
+
import { AGENT_SUBAGENT_CONCURRENCY_HEADER, agentChildRunSchema, agentChildSessionSchema, agentChildRunStartInputSchema, agentChildRunDestinationSchema, agentChildRunOutcomeSchema, agentChildRunListInputSchema, } from "@gea-ai/contract/agent-worker-tools";
|
|
7
7
|
import { notifyAgentSessionChildRun, scheduleAgentSessionChildRunChanges, queueAgentSessionChildRun, acceptAgentSessionChildRun, inheritedAgentSessionMetadata, } from "./agent-session-inbox.js";
|
|
8
8
|
import { getChildRun, getChildSession, listChildRuns, putChildRun, readChildRunIndex, readChildRuns, saveChildRunIndex, } from "./agent-session-child-store.js";
|
|
9
9
|
const runIdSchema = z.strictObject({ runId: z.uuid() });
|
|
@@ -12,6 +12,8 @@ const startSchema = agentChildRunStartInputSchema.extend({
|
|
|
12
12
|
agentKey: z.string().min(1).max(80),
|
|
13
13
|
destination: agentChildRunDestinationSchema.optional(),
|
|
14
14
|
traceContext: traceContextSchema.optional(),
|
|
15
|
+
// Only the controlled Host transport can supply a root-authorized binding.
|
|
16
|
+
authorizedSession: agentChildSessionSchema.optional(),
|
|
15
17
|
});
|
|
16
18
|
const reservationSchema = startSchema
|
|
17
19
|
.pick({
|
|
@@ -96,14 +98,38 @@ export async function handleAgentSessionChildren(request, storage) {
|
|
|
96
98
|
const runs = await readChildRuns(tx);
|
|
97
99
|
if (!index && runs.length >= 128)
|
|
98
100
|
return Response.json({ error: "child_run_limit" }, { status: 409 });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
if (input.authorizedSession &&
|
|
102
|
+
(remote ||
|
|
103
|
+
input.authorizedSession.sessionId !== input.sessionId ||
|
|
104
|
+
input.authorizedSession.destination.type !== "worker"))
|
|
105
|
+
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
106
|
+
const previous = input.authorizedSession ??
|
|
107
|
+
(input.sessionId
|
|
108
|
+
? await getChildSession(tx, input.sessionId)
|
|
109
|
+
: undefined);
|
|
110
|
+
// Hosted callers authorize the root Session. The local ledger remains
|
|
111
|
+
// the binding source for hosts without a Session directory.
|
|
112
|
+
if (input.sessionId && !previous)
|
|
113
|
+
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
114
|
+
const recordedTarget = previous
|
|
115
|
+
? (previous.target ?? previous.agentKey)
|
|
101
116
|
: undefined;
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(input.target ?? input.agentKey)))
|
|
117
|
+
if (previous &&
|
|
118
|
+
input.target !== undefined &&
|
|
119
|
+
input.target !== recordedTarget)
|
|
106
120
|
return Response.json({ error: "unknown_child_session" }, { status: 403 });
|
|
121
|
+
const resumedTarget = input.target ?? previous?.target;
|
|
122
|
+
const { authorizedSession: _authorizedSession, ...startInput } = input;
|
|
123
|
+
const continued = {
|
|
124
|
+
...startInput,
|
|
125
|
+
...(resumedTarget === undefined ? {} : { target: resumedTarget }),
|
|
126
|
+
...(previous
|
|
127
|
+
? {
|
|
128
|
+
agentKey: previous.agentKey,
|
|
129
|
+
destination: previous.destination ?? input.destination,
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
107
133
|
if (remote &&
|
|
108
134
|
previous &&
|
|
109
135
|
JSON.stringify(previous.destination) !==
|
|
@@ -132,12 +158,12 @@ export async function handleAgentSessionChildren(request, storage) {
|
|
|
132
158
|
id: reservationId,
|
|
133
159
|
sessionId: input.sessionId,
|
|
134
160
|
});
|
|
135
|
-
await tx.put(`child-reservation:${reservationId}`, reservationSchema.parse(
|
|
161
|
+
await tx.put(`child-reservation:${reservationId}`, reservationSchema.parse(continued));
|
|
136
162
|
await saveChildRunIndex(tx, index);
|
|
137
163
|
return Response.json({ reservationId });
|
|
138
164
|
}
|
|
139
165
|
const run = agentChildRunSchema.parse({
|
|
140
|
-
...
|
|
166
|
+
...continued,
|
|
141
167
|
id: crypto.randomUUID(),
|
|
142
168
|
sessionId: input.sessionId ?? crypto.randomUUID(),
|
|
143
169
|
agentKey: previous?.agentKey ?? input.agentKey,
|
|
@@ -35,6 +35,8 @@ const stateSchema = z.strictObject({
|
|
|
35
35
|
transport: agentSessionTransportSchema.nullable().default(null),
|
|
36
36
|
started: z.boolean().default(false),
|
|
37
37
|
stopped: z.boolean().default(false),
|
|
38
|
+
/** User cancel. Process recovery sets `stopped` with `interrupted` and must not set this. */
|
|
39
|
+
cancelRequested: z.boolean().default(false),
|
|
38
40
|
resumeWait: agentRunWaitResultSchema.nullable().default(null),
|
|
39
41
|
pendingStarts: z.array(z.uuid()).max(128).default([]),
|
|
40
42
|
terminal: terminalSchema.nullable().default(null),
|
|
@@ -238,9 +240,13 @@ export async function queueAgentSessionChildRun(tx, run) {
|
|
|
238
240
|
// External protocols retain their own execution and cancellation transport.
|
|
239
241
|
if (!state?.transport)
|
|
240
242
|
return false;
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
// Explicit cancellation propagates to delayed receipts. Ordinary parent
|
|
244
|
+
// completion does not revoke an already admitted child dispatch.
|
|
245
|
+
const savedOutcome = await tx.get(`inbox:outcome:${run.parentRunId}`);
|
|
246
|
+
const parentCancelled = savedOutcome !== undefined &&
|
|
247
|
+
agentSessionDispatchSchema.shape.outcome.unwrap().parse(savedOutcome)
|
|
248
|
+
.status === "cancelled";
|
|
249
|
+
if (state.stopped || parentCancelled) {
|
|
244
250
|
run.cancelRequested = true;
|
|
245
251
|
await putChildRun(tx, run);
|
|
246
252
|
}
|
|
@@ -423,7 +429,11 @@ export async function notifyAgentSessionChildRun(tx, run) {
|
|
|
423
429
|
if (!state || !run.outcome)
|
|
424
430
|
return;
|
|
425
431
|
const runs = await readInboxChildRuns(tx, state);
|
|
426
|
-
|
|
432
|
+
// Completion belongs to the originating Run. A later turn can explicitly
|
|
433
|
+
// wait for this receipt, but must not receive an unsolicited old notification.
|
|
434
|
+
const parentFinished = (await tx.get(`inbox:outcome:${run.parentRunId}`)) !== undefined;
|
|
435
|
+
if (state.stopped ||
|
|
436
|
+
(parentFinished && !state.wait?.runIds.includes(run.id))) {
|
|
427
437
|
await save(tx, state, runs);
|
|
428
438
|
return;
|
|
429
439
|
}
|
|
@@ -482,6 +492,45 @@ async function finishInbox(tx, state, runs, input) {
|
|
|
482
492
|
pendingStarts: state.pendingStarts.length,
|
|
483
493
|
});
|
|
484
494
|
}
|
|
495
|
+
// A durable user cancel wins a later transport failure or shutdown abort.
|
|
496
|
+
// Socket close can report `failed` before the worker observes the abort,
|
|
497
|
+
// and a request abort without cancelRequestedAt is only an interruption.
|
|
498
|
+
// Release can arrive after cancelRequestedAt is stored but before the inbox
|
|
499
|
+
// flag. An interrupted stop (process recovery or graceful shutdown) sets
|
|
500
|
+
// `stopped` without that flag and must stay failed or interrupted.
|
|
501
|
+
if (outcome?.status === "failed" ||
|
|
502
|
+
outcome?.status === "cancelled" ||
|
|
503
|
+
outcome?.status === "interrupted") {
|
|
504
|
+
const storedTurn = await tx.get("execution-v2:active-turn");
|
|
505
|
+
const turn = storedTurn === undefined
|
|
506
|
+
? null
|
|
507
|
+
: agentSessionExecutionTurnSchema.parse(storedTurn);
|
|
508
|
+
const userCancel = state.cancelRequested ||
|
|
509
|
+
(turn?.runId === state.execution.turnId &&
|
|
510
|
+
Boolean(turn.cancelRequestedAt));
|
|
511
|
+
if (userCancel) {
|
|
512
|
+
if (outcome.status !== "cancelled")
|
|
513
|
+
outcome = { ...outcome, status: "cancelled" };
|
|
514
|
+
state.cancelRequested = true;
|
|
515
|
+
state.stopped = true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// A later transport failure must not replace a cancel this turn already sealed.
|
|
519
|
+
if (outcome?.status === "failed" || outcome?.status === "interrupted") {
|
|
520
|
+
const turnId = state.execution.turnId;
|
|
521
|
+
const sealedCancelled = [state.terminal, ...state.terminalQueue].some((entry) => entry?.execution.turnId === turnId &&
|
|
522
|
+
entry.outcome.status === "cancelled");
|
|
523
|
+
const stored = sealedCancelled
|
|
524
|
+
? undefined
|
|
525
|
+
: await tx.get(`inbox:outcome:${turnId}`);
|
|
526
|
+
const storedOutcome = stored === undefined
|
|
527
|
+
? null
|
|
528
|
+
: agentSessionDispatchSchema.shape.outcome.unwrap().safeParse(stored);
|
|
529
|
+
if (sealedCancelled || storedOutcome?.data?.status === "cancelled") {
|
|
530
|
+
outcome = { ...outcome, status: "cancelled" };
|
|
531
|
+
state.stopped = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
485
534
|
if (inputWait && !outcome && !state.stopped) {
|
|
486
535
|
state.inputWait = {
|
|
487
536
|
turnId: state.execution.turnId,
|
|
@@ -599,6 +648,7 @@ export async function handleAgentSessionInboxRequest(request, storage) {
|
|
|
599
648
|
transport: null,
|
|
600
649
|
started: false,
|
|
601
650
|
stopped: false,
|
|
651
|
+
cancelRequested: false,
|
|
602
652
|
resumeWait: null,
|
|
603
653
|
terminal: null,
|
|
604
654
|
draining: null,
|
|
@@ -707,8 +757,10 @@ export async function handleAgentSessionInboxRequest(request, storage) {
|
|
|
707
757
|
state.inputWait = null;
|
|
708
758
|
state.inputNotification = null;
|
|
709
759
|
state.resumeWait = null;
|
|
710
|
-
if (!state.draining)
|
|
760
|
+
if (!state.draining) {
|
|
711
761
|
state.stopped = false;
|
|
762
|
+
state.cancelRequested = false;
|
|
763
|
+
}
|
|
712
764
|
state.started = path === "/v1/inbox/start";
|
|
713
765
|
if (input.message)
|
|
714
766
|
await tx.put(`inbox:message:${input.message.id}`, input.message);
|
|
@@ -775,8 +827,10 @@ export async function handleAgentSessionInboxRequest(request, storage) {
|
|
|
775
827
|
if (new TextEncoder().encode(JSON.stringify(state) + JSON.stringify(message)).byteLength > 2_000_000)
|
|
776
828
|
return Response.json({ error: "inbox_full" }, { status: 429 });
|
|
777
829
|
state.messages.push(message);
|
|
778
|
-
if (!state.draining)
|
|
830
|
+
if (!state.draining) {
|
|
779
831
|
state.stopped = false;
|
|
832
|
+
state.cancelRequested = false;
|
|
833
|
+
}
|
|
780
834
|
await tx.put(key, message);
|
|
781
835
|
await save(tx, state, runs);
|
|
782
836
|
return Response.json({ id: message.id, duplicate: false });
|
|
@@ -805,6 +859,7 @@ export async function handleAgentSessionInboxRequest(request, storage) {
|
|
|
805
859
|
const joining = state.wait?.toolCallId === null;
|
|
806
860
|
await consumeInputRequest(tx, state, runs, result);
|
|
807
861
|
state.stopped = false;
|
|
862
|
+
state.cancelRequested = false;
|
|
808
863
|
state.execution = {
|
|
809
864
|
id: crypto.randomUUID(),
|
|
810
865
|
metadata: state.metadata,
|
|
@@ -847,6 +902,8 @@ export async function handleAgentSessionInboxRequest(request, storage) {
|
|
|
847
902
|
state.draining?.execution.turnId))
|
|
848
903
|
return Response.json({ cancelled: false });
|
|
849
904
|
state.stopped = true;
|
|
905
|
+
if (!input.interrupted)
|
|
906
|
+
state.cancelRequested = true;
|
|
850
907
|
if (input.cancelPending)
|
|
851
908
|
state.messages = [];
|
|
852
909
|
if (state.wait?.toolCallId)
|
package/dist/agent-worker.js
CHANGED
|
@@ -14,6 +14,7 @@ import { agentToolOutputOffloadOptionsSchema } from "@gea-ai/contract/agent-cont
|
|
|
14
14
|
import { resolveAgentToolOutputOffload } from "@gea-ai/contract/agent-context";
|
|
15
15
|
import { decodeAgentModelProxyValue, encodeAgentModelProxyValue, } from "@gea-ai/contract/agent-model-proxy";
|
|
16
16
|
import { createGeaAI, modelDescriptorSchema } from "./gea-ai.js";
|
|
17
|
+
import { hostRejectionError } from "./host-rejection.js";
|
|
17
18
|
import { storeToolModelOutput, resolveToolArtifacts, } from "./tool-model-output.js";
|
|
18
19
|
import { mcpToolModelOutput } from "./mcp-tool-output.js";
|
|
19
20
|
import { AgentMediaReadError, readAgentMedia } from "./agent-media-read.js";
|
|
@@ -2340,7 +2341,7 @@ function createHostBackedAgentTools(input) {
|
|
|
2340
2341
|
async function requireHostJson(host, path, init) {
|
|
2341
2342
|
const response = await host.fetch(`${HOST_PATH}${path}`, init);
|
|
2342
2343
|
if (!response.ok)
|
|
2343
|
-
throw
|
|
2344
|
+
throw await hostRejectionError(path, response);
|
|
2344
2345
|
return decodeWireValue(await response.json());
|
|
2345
2346
|
}
|
|
2346
2347
|
async function createGeaCoreConnection(host, publicModel, requestOptions, signal, maxOutputTokens) {
|
|
@@ -2762,12 +2763,38 @@ export async function runAgentWorkerRequest(input) {
|
|
|
2762
2763
|
});
|
|
2763
2764
|
if (input.inboxExecution) {
|
|
2764
2765
|
const inboxStartedAt = performance.now();
|
|
2765
|
-
|
|
2766
|
+
let status = termination === "cancelled"
|
|
2766
2767
|
? "cancelled"
|
|
2767
|
-
: termination === "
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2768
|
+
: termination === "interrupted"
|
|
2769
|
+
? "interrupted"
|
|
2770
|
+
: termination === "failed" ||
|
|
2771
|
+
results.some((result) => result.status === "rejected")
|
|
2772
|
+
? "failed"
|
|
2773
|
+
: inboxOutcome.status;
|
|
2774
|
+
// A request abort is interrupted until a durable user cancel is
|
|
2775
|
+
// visible. Parent cancel sets cancelRequestedAt before aborting
|
|
2776
|
+
// the fetch, so this recheck still seals that run as cancelled.
|
|
2777
|
+
if ((status === "failed" || status === "interrupted") &&
|
|
2778
|
+
input.sessionExecution) {
|
|
2779
|
+
try {
|
|
2780
|
+
const state = await requestSessionJson({
|
|
2781
|
+
chatId: sessionChatId,
|
|
2782
|
+
method: "GET",
|
|
2783
|
+
namespace: input.sessionNamespace,
|
|
2784
|
+
path: "/v2/state?view=metadata",
|
|
2785
|
+
schema: agentSessionExecutionStateSchema,
|
|
2786
|
+
});
|
|
2787
|
+
const active = state.activeTurn;
|
|
2788
|
+
if (active?.cancelRequestedAt &&
|
|
2789
|
+
active.runId === input.sessionExecution.runId &&
|
|
2790
|
+
active.attemptId === input.sessionExecution.attemptId)
|
|
2791
|
+
status = "cancelled";
|
|
2792
|
+
}
|
|
2793
|
+
catch {
|
|
2794
|
+
// A metadata miss must not skip release. finishInbox remaps a
|
|
2795
|
+
// failed or interrupted outcome when the same cancel is durable.
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2771
2798
|
invocationSpan?.setAttribute("gea.session.status", status);
|
|
2772
2799
|
const inbox = {
|
|
2773
2800
|
executionId: input.inboxExecution.id,
|
|
@@ -2900,12 +2927,16 @@ export async function runAgentWorkerRequest(input) {
|
|
|
2900
2927
|
stopComputer = stop;
|
|
2901
2928
|
}, (outcome) => {
|
|
2902
2929
|
inboxOutcome = outcome;
|
|
2903
|
-
if (outcome.status === "failed" ||
|
|
2930
|
+
if (outcome.status === "failed" ||
|
|
2931
|
+
outcome.status === "cancelled" ||
|
|
2932
|
+
outcome.status === "interrupted")
|
|
2904
2933
|
termination ??= outcome.status;
|
|
2905
2934
|
}, invocationTiming);
|
|
2906
2935
|
}
|
|
2907
2936
|
catch (error) {
|
|
2908
|
-
|
|
2937
|
+
// Lease expiry and an explicit cancel assign termination before they
|
|
2938
|
+
// abort. A bare request abort is process shutdown, not a user cancel.
|
|
2939
|
+
termination ??= signal.aborted ? "interrupted" : "failed";
|
|
2909
2940
|
cancellation.abort(error);
|
|
2910
2941
|
try {
|
|
2911
2942
|
await finish();
|
|
@@ -2938,7 +2969,7 @@ export async function runAgentWorkerRequest(input) {
|
|
|
2938
2969
|
throw failure.reason;
|
|
2939
2970
|
};
|
|
2940
2971
|
const cancel = (reason) => {
|
|
2941
|
-
termination ??= "
|
|
2972
|
+
termination ??= "interrupted";
|
|
2942
2973
|
return stop(reason);
|
|
2943
2974
|
};
|
|
2944
2975
|
const onAbort = () => waitUntil(cancel(signal.reason));
|
|
@@ -2966,7 +2997,7 @@ export async function runAgentWorkerRequest(input) {
|
|
|
2966
2997
|
controller.close();
|
|
2967
2998
|
}
|
|
2968
2999
|
catch (error) {
|
|
2969
|
-
termination ??= "failed";
|
|
3000
|
+
termination ??= signal.aborted ? "interrupted" : "failed";
|
|
2970
3001
|
try {
|
|
2971
3002
|
await stop(error);
|
|
2972
3003
|
}
|
|
@@ -4120,7 +4151,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4120
4151
|
// A UI reader may cancel as soon as it receives an error chunk. Preserve
|
|
4121
4152
|
// the execution's original failure before publishing that chunk.
|
|
4122
4153
|
ownSessionOutcome({
|
|
4123
|
-
status: input.request.signal.aborted ? "
|
|
4154
|
+
status: input.request.signal.aborted ? "interrupted" : "failed",
|
|
4124
4155
|
text: executionErrorText(error),
|
|
4125
4156
|
});
|
|
4126
4157
|
recordExecutionFailure(error);
|
|
@@ -4207,6 +4238,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4207
4238
|
};
|
|
4208
4239
|
let terminalMessage;
|
|
4209
4240
|
let terminalEvent;
|
|
4241
|
+
// Inbox suspension is a live protocol frame, not a journaled response end.
|
|
4242
|
+
// Hold it until the final context commit so a failed commit cannot publish waiting.
|
|
4243
|
+
let liveFinish;
|
|
4210
4244
|
const saveResponseMessage = async ({ responseMessage, }) => {
|
|
4211
4245
|
const saveStartedAt = performance.now();
|
|
4212
4246
|
const metadata = await contextFinished;
|
|
@@ -4224,6 +4258,21 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4224
4258
|
}
|
|
4225
4259
|
if (inboxRunner?.suspended)
|
|
4226
4260
|
context.metadata.suspendedMessageId = responseMessage.id;
|
|
4261
|
+
const needsInput = !inboxRunner?.suspended &&
|
|
4262
|
+
responseMessage.parts.some((part) => "state" in part &&
|
|
4263
|
+
(part.state === "approval-requested" ||
|
|
4264
|
+
(part.state === "input-available" &&
|
|
4265
|
+
input.snapshot.tools.some((tool) => tool.type === "custom" &&
|
|
4266
|
+
tool.execution === "client" &&
|
|
4267
|
+
part.type === `tool-${tool.name}`))));
|
|
4268
|
+
// The same metadata object is held by the deferred finish frame. Do not
|
|
4269
|
+
// publish waiting until the final context commit succeeds; a failed commit
|
|
4270
|
+
// seals the call as failed and must not leave the message looking suspended.
|
|
4271
|
+
const markWaiting = !failure &&
|
|
4272
|
+
(needsInput ||
|
|
4273
|
+
Boolean(inboxRunner?.suspended && metadata.sessionStatus === "waiting"));
|
|
4274
|
+
if (markWaiting)
|
|
4275
|
+
delete metadata.sessionStatus;
|
|
4227
4276
|
responseMessage.metadata = {
|
|
4228
4277
|
...responseMessage.metadata,
|
|
4229
4278
|
...metadata,
|
|
@@ -4258,7 +4307,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4258
4307
|
// journal. Throwing from onFinish would truncate those chunks and hide
|
|
4259
4308
|
// both the error and final usage from clients.
|
|
4260
4309
|
ownSessionOutcome({
|
|
4261
|
-
status: input.request.signal.aborted ? "
|
|
4310
|
+
status: input.request.signal.aborted ? "interrupted" : "failed",
|
|
4262
4311
|
text: executionErrorText(failure.error, responseMessage.parts
|
|
4263
4312
|
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
4264
4313
|
.join("")),
|
|
@@ -4276,19 +4325,39 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4276
4325
|
}
|
|
4277
4326
|
throw error;
|
|
4278
4327
|
}
|
|
4328
|
+
if (markWaiting) {
|
|
4329
|
+
metadata.sessionStatus = "waiting";
|
|
4330
|
+
responseMessage.metadata = {
|
|
4331
|
+
...responseMessage.metadata,
|
|
4332
|
+
sessionStatus: "waiting",
|
|
4333
|
+
};
|
|
4334
|
+
if (terminalMessage) {
|
|
4335
|
+
terminalMessage = {
|
|
4336
|
+
...terminalMessage,
|
|
4337
|
+
metadata: {
|
|
4338
|
+
...terminalMessage.metadata,
|
|
4339
|
+
sessionStatus: "waiting",
|
|
4340
|
+
},
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
else if (sessionExecution) {
|
|
4344
|
+
await mutateSession({ messages: [{ ...responseMessage }] });
|
|
4345
|
+
}
|
|
4346
|
+
else {
|
|
4347
|
+
await appendSessionValue({
|
|
4348
|
+
body: { message: responseMessage, runId: payload.runId },
|
|
4349
|
+
chatId: payload.chatId,
|
|
4350
|
+
namespace: input.sessionNamespace,
|
|
4351
|
+
path: "/v1/messages",
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4279
4355
|
const contextPersistedAt = performance.now();
|
|
4280
4356
|
persistenceSpan?.setAttributes({
|
|
4281
4357
|
"gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - saveStartedAt),
|
|
4282
4358
|
"gea.session.finish.transcript_ms": Math.round(transcriptFinishedAt - transcriptStartedAt),
|
|
4283
4359
|
"gea.session.finish.context_write_ms": Math.round(contextPersistedAt - transcriptFinishedAt),
|
|
4284
4360
|
});
|
|
4285
|
-
const needsInput = !inboxRunner?.suspended &&
|
|
4286
|
-
responseMessage.parts.some((part) => "state" in part &&
|
|
4287
|
-
(part.state === "approval-requested" ||
|
|
4288
|
-
(part.state === "input-available" &&
|
|
4289
|
-
input.snapshot.tools.some((tool) => tool.type === "custom" &&
|
|
4290
|
-
tool.execution === "client" &&
|
|
4291
|
-
part.type === `tool-${tool.name}`))));
|
|
4292
4361
|
ownSessionOutcome({
|
|
4293
4362
|
...(needsInput
|
|
4294
4363
|
? {
|
|
@@ -4957,14 +5026,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4957
5026
|
finishReason: metadata.finishReason,
|
|
4958
5027
|
messageMetadata: metadata,
|
|
4959
5028
|
};
|
|
4960
|
-
if (metadata.
|
|
4961
|
-
|
|
5029
|
+
if (inboxRunner?.suspended && metadata.finishReason !== "error") {
|
|
5030
|
+
// onFinish commits context before this frame is published. The
|
|
5031
|
+
// journal keeps the resumed response's finish, not this boundary.
|
|
5032
|
+
liveFinish = chunk;
|
|
4962
5033
|
continue;
|
|
4963
5034
|
}
|
|
4964
|
-
if (
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
5035
|
+
if (metadata.finishReason === "tool-calls" ||
|
|
5036
|
+
(sessionExecution &&
|
|
5037
|
+
!input.channelJournal &&
|
|
5038
|
+
!inboxRunner?.suspended &&
|
|
5039
|
+
metadata.finishReason === "stop")) {
|
|
5040
|
+
// onFinish commits context before publishing waiting metadata.
|
|
5041
|
+
// Flush emits this frame only after that commit returns.
|
|
4968
5042
|
terminalEvent = chunk;
|
|
4969
5043
|
continue;
|
|
4970
5044
|
}
|
|
@@ -4982,16 +5056,26 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
|
|
|
4982
5056
|
controller.enqueue(event);
|
|
4983
5057
|
},
|
|
4984
5058
|
async flush(controller) {
|
|
4985
|
-
if (!terminalMessage && !terminalEvent)
|
|
5059
|
+
if (!terminalMessage && !terminalEvent && !liveFinish)
|
|
4986
5060
|
return;
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
5061
|
+
if (terminalMessage || terminalEvent) {
|
|
5062
|
+
const startedAt = performance.now();
|
|
5063
|
+
if (sessionExecution) {
|
|
5064
|
+
await mutateSession({
|
|
5065
|
+
...(terminalMessage
|
|
5066
|
+
? { messages: [{ ...terminalMessage }] }
|
|
5067
|
+
: {}),
|
|
5068
|
+
...(terminalEvent ? { events: [terminalEvent] } : {}),
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
else if (terminalEvent)
|
|
5072
|
+
await appendStreamEvent(terminalEvent);
|
|
5073
|
+
persistenceSpan?.setAttribute("gea.session.finish.transcript_ms", Math.round(performance.now() - startedAt));
|
|
5074
|
+
}
|
|
4993
5075
|
if (terminalEvent)
|
|
4994
5076
|
controller.enqueue(terminalEvent);
|
|
5077
|
+
if (liveFinish)
|
|
5078
|
+
controller.enqueue(liveFinish);
|
|
4995
5079
|
},
|
|
4996
5080
|
}));
|
|
4997
5081
|
if (engine || sessionExecution) {
|
package/dist/gea-ai.js
CHANGED
|
@@ -7,6 +7,7 @@ import { AGENT_MODEL_PROXY_PROTOCOL_VERSION, AGENT_MODEL_PROXY_TRANSPORT, agentC
|
|
|
7
7
|
import { mergeLlmJson } from "@gea-ai/contract/llm-catalog";
|
|
8
8
|
import { wrapLanguageModel } from "ai";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { hostRejectionError } from "./host-rejection.js";
|
|
10
11
|
import { addCacheControlToMessages, addCacheControlToToolList, } from "./cache-control.js";
|
|
11
12
|
const HOST_PATH = "/internal/agent-worker-host/v1";
|
|
12
13
|
const jsonObjectSchema = z.record(z.string(), z.json());
|
|
@@ -117,7 +118,7 @@ export async function createGeaAI(host, publicModel, extraBody, client, requestB
|
|
|
117
118
|
const path = `/model?model=${encodeURIComponent(publicModel)}&includeModelId=true&includeMaxOutputTokens=true&includeCatalogConfig=true`;
|
|
118
119
|
const response = await host.fetch(`${HOST_PATH}${path}`);
|
|
119
120
|
if (!response.ok)
|
|
120
|
-
throw
|
|
121
|
+
throw await hostRejectionError(path, response);
|
|
121
122
|
const descriptor = modelDescriptorSchema.parse(await response.json());
|
|
122
123
|
if ("transport" in descriptor) {
|
|
123
124
|
return createHostedGeaLanguageModel({ client, host, publicModel });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type HostRejectionDetail = {
|
|
2
|
+
code?: string;
|
|
3
|
+
reason?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare class AgentHostRejectionError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly code: string | undefined;
|
|
8
|
+
readonly reason: string | undefined;
|
|
9
|
+
constructor(path: string, status: number, detail: HostRejectionDetail);
|
|
10
|
+
}
|
|
11
|
+
export declare function hostRejectionError(path: string, response: Response): Promise<AgentHostRejectionError>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const HOST_REJECTION_DETAIL_LIMIT = 500;
|
|
4
|
+
const detailString = z.preprocess((value) => (typeof value === "string" ? value : undefined), z.string().optional());
|
|
5
|
+
const hostErrorFieldsSchema = z.object({
|
|
6
|
+
code: detailString,
|
|
7
|
+
message: detailString,
|
|
8
|
+
reason: detailString,
|
|
9
|
+
});
|
|
10
|
+
const hostErrorBodySchema = hostErrorFieldsSchema.extend({
|
|
11
|
+
error: z.unknown().optional(),
|
|
12
|
+
});
|
|
13
|
+
function clipHostDetail(value) {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return undefined;
|
|
17
|
+
return trimmed.length > HOST_REJECTION_DETAIL_LIMIT
|
|
18
|
+
? trimmed.slice(0, HOST_REJECTION_DETAIL_LIMIT)
|
|
19
|
+
: trimmed;
|
|
20
|
+
}
|
|
21
|
+
function readFields(value) {
|
|
22
|
+
const parsed = hostErrorFieldsSchema.safeParse(value);
|
|
23
|
+
if (!parsed.success)
|
|
24
|
+
return {};
|
|
25
|
+
return {
|
|
26
|
+
code: clipHostDetail(parsed.data.code ?? ""),
|
|
27
|
+
reason: clipHostDetail(parsed.data.reason ?? "") ??
|
|
28
|
+
clipHostDetail(parsed.data.message ?? ""),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function detailFromText(body) {
|
|
32
|
+
const trimmed = body.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
return {};
|
|
35
|
+
const parsed = Result.try(() => JSON.parse(trimmed));
|
|
36
|
+
if (parsed.isErr()) {
|
|
37
|
+
const reason = clipHostDetail(trimmed);
|
|
38
|
+
return reason ? { reason } : {};
|
|
39
|
+
}
|
|
40
|
+
const bodyFields = hostErrorBodySchema.safeParse(parsed.value);
|
|
41
|
+
if (!bodyFields.success)
|
|
42
|
+
return {};
|
|
43
|
+
const nested = readFields(bodyFields.data.error);
|
|
44
|
+
const top = readFields(parsed.value);
|
|
45
|
+
return {
|
|
46
|
+
code: nested.code ?? top.code,
|
|
47
|
+
reason: nested.reason ?? top.reason,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function readHostRejectionDetail(response) {
|
|
51
|
+
const body = await Result.tryPromise(() => response.text());
|
|
52
|
+
if (body.isErr())
|
|
53
|
+
return {};
|
|
54
|
+
return detailFromText(body.value);
|
|
55
|
+
}
|
|
56
|
+
function hostRejectionMessage(path, status, detail) {
|
|
57
|
+
const summary = detail.code && detail.reason
|
|
58
|
+
? `${detail.code} - ${detail.reason}`
|
|
59
|
+
: (detail.code ?? detail.reason);
|
|
60
|
+
return summary
|
|
61
|
+
? `Agent host rejected ${path} (${status}): ${summary}`
|
|
62
|
+
: `Agent host rejected ${path} (${status}).`;
|
|
63
|
+
}
|
|
64
|
+
export class AgentHostRejectionError extends Error {
|
|
65
|
+
status;
|
|
66
|
+
code;
|
|
67
|
+
reason;
|
|
68
|
+
constructor(path, status, detail) {
|
|
69
|
+
super(hostRejectionMessage(path, status, detail));
|
|
70
|
+
this.name = "AgentHostRejectionError";
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.code = detail.code;
|
|
73
|
+
this.reason = detail.reason;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function hostRejectionError(path, response) {
|
|
77
|
+
return new AgentHostRejectionError(path, response.status, await readHostRejectionDetail(response));
|
|
78
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const agentSdkVersion = "0.1.
|
|
1
|
+
export declare const agentSdkVersion = "0.1.261010-alpha.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/build-sdk-version.mjs from package.json.
|
|
2
|
-
export const agentSdkVersion = "0.1.
|
|
2
|
+
export const agentSdkVersion = "0.1.261010-alpha.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gea-ai/agent-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.261010-alpha.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"homepage": "https://musegea.com/developers",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@ai-sdk/otel": "1.0.119",
|
|
120
120
|
"@ai-sdk/provider": "4.0.19",
|
|
121
121
|
"@ai-sdk/provider-utils": "5.0.50",
|
|
122
|
-
"@gea-ai/contract": "0.1.
|
|
122
|
+
"@gea-ai/contract": "0.1.261010-alpha.0",
|
|
123
123
|
"@opentelemetry/api": "1.9.1",
|
|
124
124
|
"@opentelemetry/context-async-hooks": "2.10.0",
|
|
125
125
|
"@opentelemetry/core": "2.10.0",
|