Skip to content

Commit b1e8593

Browse files
committed
Support for upgrading an agent to a new version
1 parent 229bfc9 commit b1e8593

File tree

13 files changed

+649
-62
lines changed

13 files changed

+649
-62
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { json } from "@remix-run/server-runtime";
2+
import { $replica } from "~/db.server";
3+
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
4+
5+
export const loader = createLoaderApiRoute(
6+
{
7+
allowJWT: true,
8+
corsStrategy: "none",
9+
authorization: {
10+
action: "read",
11+
resource: () => ({ deployments: "current" }),
12+
superScopes: ["read:deployments", "read:all", "admin"],
13+
},
14+
findResource: async (_params, auth) => {
15+
const promotion = await $replica.workerDeploymentPromotion.findFirst({
16+
where: {
17+
environmentId: auth.environment.id,
18+
label: "current",
19+
},
20+
select: {
21+
deployment: {
22+
select: {
23+
friendlyId: true,
24+
createdAt: true,
25+
shortCode: true,
26+
version: true,
27+
runtime: true,
28+
runtimeVersion: true,
29+
status: true,
30+
deployedAt: true,
31+
git: true,
32+
errorData: true,
33+
},
34+
},
35+
},
36+
});
37+
38+
return promotion?.deployment ?? null;
39+
},
40+
},
41+
async ({ resource: deployment }) => {
42+
return json({
43+
id: deployment.friendlyId,
44+
createdAt: deployment.createdAt,
45+
shortCode: deployment.shortCode,
46+
version: deployment.version,
47+
runtime: deployment.runtime,
48+
runtimeVersion: deployment.runtimeVersion,
49+
status: deployment.status,
50+
deployedAt: deployment.deployedAt ?? undefined,
51+
git: deployment.git ?? undefined,
52+
error: deployment.errorData ?? undefined,
53+
});
54+
}
55+
);

packages/cli-v3/src/entryPoints/managed-index-controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ async function indexDeployment({
104104
packageVersion: buildManifest.packageVersion,
105105
cliPackageVersion: buildManifest.cliPackageVersion,
106106
tasks: workerManifest.tasks,
107+
prompts: workerManifest.prompts,
107108
queues: workerManifest.queues,
108109
sourceFiles,
109110
runtime: workerManifest.runtime,

packages/cli-v3/src/mcp/tools/agentChat.ts

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ import type { McpContext } from "../context.js";
1212

1313
// ─── In-memory chat sessions ──────────────────────────────────────
1414

15+
type ChatMessage = {
16+
id: string;
17+
role: string;
18+
parts: Array<{ type: string; [key: string]: unknown }>;
19+
};
20+
1521
type ChatSession = {
1622
runId: string;
1723
chatId: string;
1824
agentId: string;
1925
lastEventId?: string;
2026
apiClient: ApiClient;
2127
clientData?: Record<string, unknown>;
28+
/** Accumulated conversation messages for continuation payloads. */
29+
messages: ChatMessage[];
2230
};
2331

2432
const activeSessions = new Map<string, ChatSession>();
@@ -108,6 +116,7 @@ export const startAgentChatTool = {
108116
agentId: input.agentId,
109117
apiClient,
110118
clientData: input.clientData,
119+
messages: [],
111120
});
112121

113122
return {
@@ -134,6 +143,7 @@ export const startAgentChatTool = {
134143
agentId: input.agentId,
135144
apiClient,
136145
clientData: input.clientData,
146+
messages: [],
137147
});
138148

139149
return {
@@ -176,10 +186,15 @@ export const sendAgentMessageTool = {
176186
}
177187

178188
const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
189+
const userMessage: ChatMessage = {
190+
id: msgId, role: "user", parts: [{ type: "text", text: input.message }],
191+
};
192+
193+
// Track the outgoing user message
194+
session.messages.push(userMessage);
195+
179196
const messagePayload = {
180-
messages: [
181-
{ id: msgId, role: "user", parts: [{ type: "text", text: input.message }] },
182-
],
197+
messages: [userMessage],
183198
chatId: session.chatId,
184199
trigger: "submit-message",
185200
metadata: session.clientData,
@@ -194,9 +209,16 @@ export const sendAgentMessageTool = {
194209
messagePayload
195210
);
196211
} catch (sendErr: any) {
197-
// Run may have ended — trigger a new one
212+
// Run may have ended — trigger a new one with full history
198213
const result = await session.apiClient.triggerTask(session.agentId, {
199-
payload: { ...messagePayload, continuation: true, previousRunId: session.runId },
214+
payload: {
215+
messages: session.messages,
216+
chatId: session.chatId,
217+
trigger: "submit-message",
218+
metadata: session.clientData,
219+
continuation: true,
220+
previousRunId: session.runId,
221+
},
200222
options: {
201223
payloadType: "application/json",
202224
tags: [`chat:${session.chatId}`],
@@ -218,17 +240,16 @@ export const sendAgentMessageTool = {
218240
}
219241

220242
// Subscribe to the response stream and collect the full text
221-
const { text, toolCalls } = await collectAgentResponse(session);
243+
const { text, toolCalls, assistantMessage } = await collectAgentResponse(session);
222244

223-
const contents = [text];
245+
// Track the assistant response for continuation payloads
246+
session.messages.push(assistantMessage);
224247

225-
if (toolCalls.length > 0) {
226-
contents.push("");
227-
contents.push(`Tools used: ${toolCalls.join(", ")}`);
228-
}
248+
const formatted = formatAssistantParts(assistantMessage.parts);
249+
const footer = `\n\n---\nRun: ${session.runId}`;
229250

230251
return {
231-
content: [{ type: "text", text: contents.join("\n") }],
252+
content: [{ type: "text", text: formatted + footer }],
232253
};
233254
}),
234255
};
@@ -287,7 +308,7 @@ export const closeAgentChatTool = {
287308

288309
async function collectAgentResponse(
289310
session: ChatSession
290-
): Promise<{ text: string; toolCalls: string[] }> {
311+
): Promise<{ text: string; toolCalls: string[]; assistantMessage: ChatMessage }> {
291312
const baseURL = session.apiClient.baseUrl;
292313
const streamUrl = `${baseURL}/realtime/v1/streams/${session.runId}/${CHAT_STREAM_KEY}`;
293314

@@ -299,15 +320,14 @@ async function collectAgentResponse(
299320
lastEventId: session.lastEventId,
300321
});
301322

302-
try {
303-
sseStream = await subscription.subscribe();
304-
} catch (err: any) {
305-
throw err;
306-
}
323+
const sseStream = await subscription.subscribe();
307324
const reader = sseStream.getReader();
308325

309326
let text = "";
310327
const toolCalls: string[] = [];
328+
const parts: Array<{ type: string; [key: string]: unknown }> = [];
329+
// Track current text part to accumulate deltas
330+
let currentTextId: string | undefined;
311331

312332
try {
313333
while (true) {
@@ -323,25 +343,117 @@ async function collectAgentResponse(
323343
if (value.chunk != null && typeof value.chunk === "object") {
324344
const chunk = value.chunk as Record<string, unknown>;
325345

326-
if (chunk.type === "__trigger_turn_complete") {
346+
if (chunk.type === "trigger:turn-complete") {
327347
break;
328348
}
329349

350+
if (chunk.type === "trigger:upgrade-required") {
351+
// Agent requested upgrade — trigger continuation with full history
352+
const previousRunId = session.runId;
353+
const result = await session.apiClient.triggerTask(session.agentId, {
354+
payload: {
355+
messages: session.messages,
356+
chatId: session.chatId,
357+
trigger: "submit-message",
358+
metadata: session.clientData,
359+
continuation: true,
360+
previousRunId,
361+
},
362+
options: {
363+
payloadType: "application/json",
364+
tags: [`chat:${session.chatId}`],
365+
},
366+
});
367+
session.runId = result.id;
368+
session.lastEventId = undefined;
369+
reader.releaseLock();
370+
// Recurse — subscribe to the new run's stream
371+
return collectAgentResponse(session);
372+
}
373+
330374
if (chunk.type === "text-delta" && typeof chunk.delta === "string") {
331375
text += chunk.delta;
376+
// Accumulate into a text part
377+
const textId = (chunk.id as string) ?? "text";
378+
if (currentTextId !== textId) {
379+
currentTextId = textId;
380+
parts.push({ type: "text", text: chunk.delta });
381+
} else {
382+
const last = parts[parts.length - 1];
383+
if (last && last.type === "text") {
384+
last.text = (last.text as string) + chunk.delta;
385+
}
386+
}
332387
}
333388

334-
if (
335-
chunk.type === "tool-input-available" &&
336-
typeof chunk.toolName === "string"
337-
) {
389+
if (chunk.type === "tool-input-available" && typeof chunk.toolName === "string") {
338390
toolCalls.push(chunk.toolName);
391+
parts.push({
392+
type: `tool-${chunk.toolName}`,
393+
toolCallId: chunk.toolCallId as string,
394+
toolName: chunk.toolName,
395+
state: "input-available",
396+
input: chunk.input,
397+
});
398+
}
399+
400+
if (chunk.type === "tool-output-available" && typeof chunk.toolCallId === "string") {
401+
// Update existing tool part with output
402+
const toolPart = parts.find(
403+
(p) => p.toolCallId === chunk.toolCallId
404+
);
405+
if (toolPart) {
406+
toolPart.state = "output-available";
407+
toolPart.output = chunk.output;
408+
}
339409
}
340410
}
341411
}
342412
} finally {
343413
reader.releaseLock();
344414
}
345415

346-
return { text, toolCalls };
416+
const assistantMessage: ChatMessage = {
417+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
418+
role: "assistant",
419+
parts: parts.length > 0 ? parts : [{ type: "text", text }],
420+
};
421+
422+
return { text, toolCalls, assistantMessage };
423+
}
424+
425+
// ─── Response formatter ──────────────────────────────────────────
426+
427+
function formatAssistantParts(
428+
parts: Array<{ type: string; [key: string]: unknown }>
429+
): string {
430+
const sections: string[] = [];
431+
432+
for (const part of parts) {
433+
if (part.type === "text" && typeof part.text === "string" && part.text) {
434+
sections.push(part.text);
435+
} else if (part.type.startsWith("tool-") && part.toolName) {
436+
const name = part.toolName as string;
437+
const input = part.input;
438+
const output = part.output;
439+
440+
let toolSection = `[Tool: ${name}]`;
441+
if (input != null) {
442+
toolSection += `\nInput: ${compactJson(input)}`;
443+
}
444+
if (output != null) {
445+
toolSection += `\nOutput: ${compactJson(output)}`;
446+
}
447+
sections.push(toolSection);
448+
}
449+
}
450+
451+
return sections.join("\n\n");
452+
}
453+
454+
function compactJson(value: unknown): string {
455+
const str = JSON.stringify(value);
456+
// Keep short values inline, truncate long ones
457+
if (str.length <= 200) return str;
458+
return str.slice(0, 200) + "…";
347459
}

packages/core/src/v3/apiClient/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ApiDeploymentListOptions,
77
ApiDeploymentListResponseItem,
88
ApiDeploymentListSearchParams,
9+
RetrieveCurrentDeploymentResponseBody,
910
AppendToStreamResponseBody,
1011
BatchItemNDJSON,
1112
BatchTaskRunExecutionResult,
@@ -1340,6 +1341,18 @@ export class ApiClient {
13401341
);
13411342
}
13421343

1344+
retrieveCurrentDeployment(requestOptions?: ZodFetchOptions) {
1345+
return zodfetch(
1346+
RetrieveCurrentDeploymentResponseBody,
1347+
`${this.baseUrl}/api/v1/deployments/current`,
1348+
{
1349+
method: "GET",
1350+
headers: this.#getHeaders(false),
1351+
},
1352+
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
1353+
);
1354+
}
1355+
13431356
async fetchStream<T>(
13441357
runId: string,
13451358
streamKey: string,

packages/core/src/v3/schemas/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,9 @@ export const ApiDeploymentListResponseItem = z.object({
15741574

15751575
export type ApiDeploymentListResponseItem = z.infer<typeof ApiDeploymentListResponseItem>;
15761576

1577+
export const RetrieveCurrentDeploymentResponseBody = ApiDeploymentListResponseItem;
1578+
export type RetrieveCurrentDeploymentResponseBody = ApiDeploymentListResponseItem;
1579+
15771580
export const ApiBranchListResponseBody = z.object({
15781581
branches: z.array(
15791582
z.object({

0 commit comments

Comments
 (0)