Skip to content

Commit 3b3ef36

Browse files
committed
refactor(agent): simplify spawn wiring
1 parent f059311 commit 3b3ef36

24 files changed

Lines changed: 197 additions & 488 deletions

packages/junior/migrations/0008_agent_invocations.sql

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ CREATE TABLE "junior_agent_bindings" (
33
"name" text NOT NULL,
44
"child_conversation_id" text NOT NULL,
55
"reasoning_level" text,
6-
"created_at" timestamp with time zone NOT NULL,
7-
"updated_at" timestamp with time zone NOT NULL,
86
CONSTRAINT "junior_agent_bindings_parent_conversation_id_name_pk" PRIMARY KEY("parent_conversation_id","name")
97
);
108
--> statement-breakpoint
119
CREATE TABLE "junior_agent_invocations" (
1210
"invocation_id" text PRIMARY KEY NOT NULL,
13-
"idempotency_key" text NOT NULL,
1411
"parent_conversation_id" text NOT NULL,
1512
"child_conversation_id" text NOT NULL,
1613
"agent_name" text,
@@ -35,6 +32,5 @@ ALTER TABLE "junior_agent_bindings" ADD CONSTRAINT "junior_agent_bindings_child_
3532
ALTER TABLE "junior_agent_invocations" ADD CONSTRAINT "junior_agent_invocations_parent_conversation_id_junior_conversations_conversation_id_fk" FOREIGN KEY ("parent_conversation_id") REFERENCES "public"."junior_conversations"("conversation_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
3633
ALTER TABLE "junior_agent_invocations" ADD CONSTRAINT "junior_agent_invocations_child_conversation_id_junior_conversations_conversation_id_fk" FOREIGN KEY ("child_conversation_id") REFERENCES "public"."junior_conversations"("conversation_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
3734
CREATE UNIQUE INDEX "junior_agent_bindings_child_idx" ON "junior_agent_bindings" USING btree ("child_conversation_id");--> statement-breakpoint
38-
CREATE UNIQUE INDEX "junior_agent_invocations_idempotency_idx" ON "junior_agent_invocations" USING btree ("parent_conversation_id","idempotency_key");--> statement-breakpoint
3935
CREATE INDEX "junior_agent_invocations_child_idx" ON "junior_agent_invocations" USING btree ("child_conversation_id");--> statement-breakpoint
4036
CREATE INDEX "junior_agent_invocations_mailbox_idx" ON "junior_agent_invocations" USING btree ("mailbox_status","created_at");

packages/junior/migrations/meta/0008_snapshot.json

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "90f9bca0-661a-4e74-a368-5a4b574b6f96",
2+
"id": "eeef7202-0cb2-4b49-bbc8-1d52e10eff0f",
33
"prevId": "b85697dd-3399-4757-93bf-3bdad67612ea",
44
"version": "7",
55
"dialect": "postgresql",
@@ -31,18 +31,6 @@
3131
"type": "text",
3232
"primaryKey": false,
3333
"notNull": false
34-
},
35-
"created_at": {
36-
"name": "created_at",
37-
"type": "timestamp with time zone",
38-
"primaryKey": false,
39-
"notNull": true
40-
},
41-
"updated_at": {
42-
"name": "updated_at",
43-
"type": "timestamp with time zone",
44-
"primaryKey": false,
45-
"notNull": true
4634
}
4735
},
4836
"indexes": {
@@ -103,12 +91,6 @@
10391
"primaryKey": true,
10492
"notNull": true
10593
},
106-
"idempotency_key": {
107-
"name": "idempotency_key",
108-
"type": "text",
109-
"primaryKey": false,
110-
"notNull": true
111-
},
11294
"parent_conversation_id": {
11395
"name": "parent_conversation_id",
11496
"type": "text",
@@ -213,27 +195,6 @@
213195
}
214196
},
215197
"indexes": {
216-
"junior_agent_invocations_idempotency_idx": {
217-
"name": "junior_agent_invocations_idempotency_idx",
218-
"columns": [
219-
{
220-
"expression": "parent_conversation_id",
221-
"isExpression": false,
222-
"asc": true,
223-
"nulls": "last"
224-
},
225-
{
226-
"expression": "idempotency_key",
227-
"isExpression": false,
228-
"asc": true,
229-
"nulls": "last"
230-
}
231-
],
232-
"isUnique": true,
233-
"concurrently": false,
234-
"method": "btree",
235-
"with": {}
236-
},
237198
"junior_agent_invocations_child_idx": {
238199
"name": "junior_agent_invocations_child_idx",
239200
"columns": [

packages/junior/migrations/meta/_journal.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
{
6262
"idx": 8,
6363
"version": "7",
64-
"when": 1785117467102,
64+
"when": 1785171129009,
6565
"tag": "0008_agent_invocations",
6666
"breakpoints": true
6767
}

packages/junior/src/app.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,7 @@ import {
6262
type VercelConversationWorkCallbackOptions,
6363
} from "@/chat/task-execution/vercel-callback";
6464
import { getVercelConversationWorkQueue } from "@/chat/task-execution/vercel-queue";
65-
import {
66-
bindAgentSpawnControl,
67-
createAgentInvocationCreator,
68-
} from "@/chat/agent-invocations/spawn";
65+
import { bindSpawnAgent } from "@/chat/agent-invocations/spawn";
6966
import {
7067
createVercelPluginTaskCallback,
7168
registerVercelPluginTaskDevConsumer,
@@ -626,12 +623,9 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
626623
const waitUntil = options?.waitUntil ?? (await defaultWaitUntil());
627624
const tracePropagation = { domains: sandboxEgressTracePropagationDomains };
628625
const conversationWorkQueue = getVercelConversationWorkQueue();
629-
const agentInvocationCreator = createAgentInvocationCreator({
630-
queue: conversationWorkQueue,
631-
});
632626
const agentRunner = createAgentRunner(executeAgentRun, {
633627
bindSpawnAgent: (request) =>
634-
bindAgentSpawnControl(request, agentInvocationCreator),
628+
bindSpawnAgent(request, { queue: conversationWorkQueue }),
635629
tracePropagation,
636630
});
637631
const runtimeServiceOverrides = {

packages/junior/src/chat/agent-invocations/spawn.ts

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,40 @@ import type { StateAdapter } from "chat";
22
import type { ConversationStore } from "@/chat/conversations/store";
33
import type {
44
AgentRunRequest,
5-
AgentSpawnControl,
6-
AgentSpawnInput,
7-
AgentSpawnResult,
5+
SpawnAgent,
6+
SpawnAgentInput,
87
} from "@/chat/agent/request";
98
import {
109
actorFromRouting,
1110
toolInvocationDestination,
1211
} from "@/chat/agent/request";
1312
import type { ConversationWorkQueue } from "@/chat/task-execution/queue";
14-
import type { CreateAgentInvocationInput } from "./types";
1513
import { createAndEnqueueAgentInvocation } from "./work";
1614

17-
export interface AgentInvocationCreator {
18-
create(input: CreateAgentInvocationInput): Promise<AgentSpawnResult>;
19-
}
20-
21-
interface AgentInvocationCreatorOptions {
15+
type SpawnOptions = {
2216
conversationStore?: ConversationStore;
2317
queue: ConversationWorkQueue | (() => ConversationWorkQueue);
2418
state?: StateAdapter;
25-
}
26-
27-
/** Create the narrow durable invocation capability used by agent runners. */
28-
export function createAgentInvocationCreator(
29-
options: AgentInvocationCreatorOptions,
30-
): AgentInvocationCreator {
31-
return {
32-
async create(input) {
33-
const queue =
34-
typeof options.queue === "function" ? options.queue() : options.queue;
35-
const created = await createAndEnqueueAgentInvocation(input, {
36-
...options,
37-
queue,
38-
});
39-
return {
40-
agentName: created.invocation.agentName,
41-
childConversationId: created.invocation.childConversationId,
42-
invocationId: created.invocation.invocationId,
43-
replayed: created.status === "existing",
44-
status: created.invocation.status,
45-
};
46-
},
47-
};
48-
}
19+
};
4920

5021
/** Bind one run's runtime-owned authority to the model-safe spawn capability. */
51-
export function bindAgentSpawnControl(
22+
export function bindSpawnAgent(
5223
request: AgentRunRequest,
53-
creator: AgentInvocationCreator,
54-
): AgentSpawnControl | undefined {
24+
options: SpawnOptions,
25+
): SpawnAgent | undefined {
5526
const actor = actorFromRouting(request.routing);
5627
if (!actor) {
5728
return undefined;
5829
}
59-
return {
60-
async execute(
61-
input: AgentSpawnInput,
62-
options: { signal?: AbortSignal; toolCallId: string },
63-
) {
64-
options.signal?.throwIfAborted();
65-
return await creator.create({
30+
return async (
31+
input: SpawnAgentInput,
32+
call: { signal?: AbortSignal; toolCallId: string },
33+
) => {
34+
call.signal?.throwIfAborted();
35+
const queue =
36+
typeof options.queue === "function" ? options.queue() : options.queue;
37+
const invocation = await createAndEnqueueAgentInvocation(
38+
{
6639
actor,
6740
...(input.name ? { agentName: input.name } : {}),
6841
...(request.routing.credentialContext
@@ -74,14 +47,16 @@ export function bindAgentSpawnControl(
7447
destinationVisibility: request.routing.destinationVisibility,
7548
}
7649
: {}),
77-
idempotencyKey: `${request.turnId}:${options.toolCallId}`,
50+
idempotencyKey: `${request.turnId}:${call.toolCallId}`,
7851
input: input.task,
7952
parentConversationId: request.conversationId,
8053
...(input.reasoningLevel
8154
? { reasoningLevel: input.reasoningLevel }
8255
: {}),
8356
source: request.routing.source,
84-
});
85-
},
57+
},
58+
{ ...options, queue },
59+
);
60+
return { invocationId: invocation.invocationId };
8661
};
8762
}

packages/junior/src/chat/agent-invocations/store.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function getAgentInvocationId(
4242
}
4343

4444
/** Return the stable child identity for one invocation without a named binding. */
45-
export function getEphemeralAgentConversationId(invocationId: string): string {
45+
export function getUnnamedAgentConversationId(invocationId: string): string {
4646
return stableId("agent", invocationId);
4747
}
4848

@@ -69,11 +69,9 @@ function bindingFromRow(
6969
): AgentBinding {
7070
return agentBindingSchema.parse({
7171
childConversationId: row.childConversationId,
72-
createdAtMs: row.createdAt.getTime(),
7372
name: row.name,
7473
parentConversationId: row.parentConversationId,
7574
...(row.reasoningLevel ? { reasoningLevel: row.reasoningLevel } : {}),
76-
updatedAtMs: row.updatedAt.getTime(),
7775
});
7876
}
7977

@@ -93,7 +91,6 @@ function invocationFromRow(
9391
? { destinationVisibility: row.destinationVisibility }
9492
: {}),
9593
...(row.errorMessage !== null ? { errorMessage: row.errorMessage } : {}),
96-
idempotencyKey: row.idempotencyKey,
9794
input: row.input,
9895
invocationId: row.invocationId,
9996
mailboxStatus: row.mailboxStatus,
@@ -114,7 +111,6 @@ function sameCreateInput(
114111
): boolean {
115112
return (
116113
invocation.parentConversationId === input.parentConversationId &&
117-
invocation.idempotencyKey === input.idempotencyKey &&
118114
invocation.agentName === input.agentName &&
119115
invocation.input === input.input &&
120116
invocation.reasoningLevel === input.reasoningLevel &&
@@ -129,7 +125,7 @@ function sameCreateInput(
129125
}
130126

131127
/** Read one named child binding in its parent-agent scope. */
132-
export async function getAgentBinding(args: {
128+
async function getAgentBinding(args: {
133129
name: string;
134130
parentConversationId: string;
135131
}): Promise<AgentBinding | undefined> {
@@ -205,20 +201,20 @@ async function getNonTerminalAgentInvocationForConversation(
205201

206202
/**
207203
* Create or replay one invocation, reusing named child conversations and
208-
* keeping ephemeral child identities scoped to the invocation.
204+
* keeping unnamed child identities scoped to the invocation.
209205
*/
210206
export async function createAgentInvocation(
211207
rawInput: CreateAgentInvocationInput,
212208
nowMs = Date.now(),
213-
): Promise<{ invocation: AgentInvocation; status: "created" | "existing" }> {
209+
): Promise<AgentInvocation> {
214210
const input = createAgentInvocationSchema.parse(rawInput);
215211
const invocationId = getAgentInvocationId(
216212
input.parentConversationId,
217213
input.idempotencyKey,
218214
);
219215
const childConversationId = input.agentName
220216
? getNamedAgentConversationId(input.parentConversationId, input.agentName)
221-
: getEphemeralAgentConversationId(invocationId);
217+
: getUnnamedAgentConversationId(invocationId);
222218
const lockName = `${CREATE_LOCK_PREFIX}:${
223219
input.agentName ? childConversationId : invocationId
224220
}`;
@@ -248,7 +244,7 @@ export async function createAgentInvocation(
248244
`Agent invocation idempotency key was reused with different input for ${invocationId}`,
249245
);
250246
}
251-
return { invocation: existing, status: "existing" };
247+
return existing;
252248
}
253249

254250
await getConversationStore().createChild({
@@ -264,11 +260,9 @@ export async function createAgentInvocation(
264260
.insert(juniorAgentBindings)
265261
.values({
266262
childConversationId,
267-
createdAt: new Date(nowMs),
268263
name: input.agentName,
269264
parentConversationId: input.parentConversationId,
270265
reasoningLevel: effectiveInput.reasoningLevel ?? null,
271-
updatedAt: new Date(nowMs),
272266
})
273267
.onConflictDoNothing();
274268
const binding = await getAgentBinding({
@@ -297,7 +291,6 @@ export async function createAgentInvocation(
297291
.insert(juniorAgentInvocations)
298292
.values({
299293
invocationId,
300-
idempotencyKey: input.idempotencyKey,
301294
parentConversationId: input.parentConversationId,
302295
childConversationId,
303296
agentName: input.agentName ?? null,
@@ -319,7 +312,7 @@ export async function createAgentInvocation(
319312
if (!invocation || !sameCreateInput(invocation, effectiveInput)) {
320313
throw new Error(`Agent invocation creation raced for ${invocationId}`);
321314
}
322-
return { invocation, status: "created" };
315+
return invocation;
323316
});
324317
}
325318

packages/junior/src/chat/agent-invocations/types.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,16 @@ const exactStringSchema = z
1818

1919
export const agentNameSchema = exactStringSchema.max(64);
2020

21-
export const agentInvocationStatusSchema = z.enum(AGENT_INVOCATION_STATUSES);
22-
23-
export const agentInvocationMailboxStatusSchema = z.enum(
21+
const agentInvocationMailboxStatusSchema = z.enum(
2422
AGENT_INVOCATION_MAILBOX_STATUSES,
2523
);
2624

2725
export const agentBindingSchema = z
2826
.object({
2927
childConversationId: exactStringSchema,
30-
createdAtMs: z.number().finite(),
3128
name: agentNameSchema,
3229
parentConversationId: exactStringSchema,
3330
reasoningLevel: z.enum(TURN_REASONING_LEVELS).optional(),
34-
updatedAtMs: z.number().finite(),
3531
})
3632
.strict();
3733

@@ -44,7 +40,6 @@ const agentInvocationBaseSchema = z
4440
credentialContext: credentialContextSchema.optional(),
4541
destination: destinationSchema,
4642
destinationVisibility: z.enum(["public", "private"]).optional(),
47-
idempotencyKey: exactStringSchema,
4843
input: exactStringSchema,
4944
invocationId: exactStringSchema,
5045
mailboxStatus: agentInvocationMailboxStatusSchema,
@@ -88,12 +83,7 @@ export const createAgentInvocationSchema = z
8883

8984
export type AgentBinding = z.output<typeof agentBindingSchema>;
9085
export type AgentInvocation = z.output<typeof agentInvocationSchema>;
91-
export type AgentInvocationStatus = z.output<
92-
typeof agentInvocationStatusSchema
93-
>;
94-
export type AgentInvocationMailboxStatus = z.output<
95-
typeof agentInvocationMailboxStatusSchema
96-
>;
86+
export type AgentInvocationStatus = (typeof AGENT_INVOCATION_STATUSES)[number];
9787
export type CreateAgentInvocationInput = z.input<
9888
typeof createAgentInvocationSchema
9989
>;

0 commit comments

Comments
 (0)