diff --git a/lib/client/index.ts b/lib/client/index.ts index df24b6b..e3b38e4 100644 --- a/lib/client/index.ts +++ b/lib/client/index.ts @@ -310,6 +310,8 @@ export interface SpawnAgentRequest { agentType: string; relatedAgentId?: string; input?: Record; + /** Optional custom ID for the agent. If an agent with this ID exists, it will be resumed instead of created. */ + id?: string; } export interface InvokeRequest { @@ -497,7 +499,7 @@ export class AgencyClient { ) {} private get path(): string { - return `${this.baseUrl}/agency/${this.agencyId}`; + return `${this.baseUrl}/agency/${encodeURIComponent(this.agencyId)}`; } private async request( diff --git a/lib/runtime/agency.ts b/lib/runtime/agency.ts index 7a313ad..0faf905 100644 --- a/lib/runtime/agency.ts +++ b/lib/runtime/agency.ts @@ -731,9 +731,10 @@ export class Agency extends Agent { requestContext?: ThreadRequestContext; input?: Record; relatedAgentId?: string; + id?: string; }; - return this.spawnAgent(body.agentType, body.requestContext, body.input, body.relatedAgentId); + return this.spawnAgent(body.agentType, body.requestContext, body.input, body.relatedAgentId, body.id); } private async handleDeleteAgent(agentId: string): Promise { @@ -868,10 +869,51 @@ export class Agency extends Agent { agentType: string, requestContext?: ThreadRequestContext, input?: Record, - relatedAgentId?: string + relatedAgentId?: string, + providedId?: string ): Promise { - const id = crypto.randomUUID(); + const id = providedId || crypto.randomUUID(); const createdAt = Date.now(); + + // Check if agent with this ID already exists + if (providedId) { + const existing = this.sql<{ id: string }>` + SELECT id FROM agents WHERE id = ${providedId} + `; + if (existing.length > 0) { + // Agent exists - invoke it instead of creating + const stub = await getAgentByName(this.exports.HubAgent, providedId); + + if (input) { + const userMessage = + typeof input.message === "string" + ? input.message + : JSON.stringify(input); + + await stub.fetch( + new Request("http://do/invoke", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + messages: [{ role: "user", content: userMessage }], + }), + }) + ); + } + + // Return existing agent info + const meta = this.sql<{ metadata: string; created_at: number }>` + SELECT metadata, created_at FROM agents WHERE id = ${providedId} + `[0]; + + return Response.json({ + id: providedId, + createdAt: new Date(meta.created_at).toISOString(), + agentType, + resumed: true, + }, { status: 200 }); + } + } const meta = { request: requestContext, diff --git a/lib/runtime/plugins/context.ts b/lib/runtime/plugins/context.ts index 57c1cf8..f870e12 100644 --- a/lib/runtime/plugins/context.ts +++ b/lib/runtime/plugins/context.ts @@ -247,8 +247,8 @@ export const context: AgentPlugin = { const recentMessages = store.getMessagesAfter(checkpointEndSeq); plan.setMessages([ { - role: "assistant", - content: `[Conversation Summary]\n${checkpoint.summary}`, + role: "user", + content: `[Previous Conversation Summary]\n${checkpoint.summary}\n\n---\nContinue from where we left off.`, }, ...recentMessages.filter((m: ChatMessage) => m.role !== "system"), ]); @@ -349,10 +349,11 @@ export const context: AgentPlugin = { }); // Set messages for this request: summary + recent messages + // Use "user" role for summary since many LLMs don't allow starting with "assistant" plan.setMessages([ { - role: "assistant", - content: `[Conversation Summary]\n${summary}`, + role: "user", + content: `[Previous Conversation Summary]\n${summary}\n\n---\nContinue from where we left off.`, }, ...toKeep.filter((m: ChatMessage) => m.role !== "system"), ]); diff --git a/lib/runtime/worker.ts b/lib/runtime/worker.ts index d500aa2..87e1d45 100644 --- a/lib/runtime/worker.ts +++ b/lib/runtime/worker.ts @@ -156,7 +156,36 @@ const createAgency = async (req: IRequest, { env }: RequestContext) => { async function getAgencyStub(agencyId: string, ctx: CfCtx): Promise> { - return getAgentByName(ctx.exports.Agency, agencyId); + // Decode in case the agency ID contains slashes (e.g., "owner/repo") + const decodedId = decodeURIComponent(agencyId); + return getAgentByName(ctx.exports.Agency, decodedId); +} + +/** Check if an agency exists (has been explicitly created via POST /agencies) */ +async function agencyExists(agencyId: string, env: HandlerEnv): Promise { + if (!env.FS) return true; // No R2 bucket = skip check + const metaObj = await env.FS.head(`${agencyId}/.agency.json`); + return metaObj !== null; +} + +/** + * Require agency to exist before proceeding. Returns 404 Response if not found. + * Use in route handlers: const error = await requireAgency(...); if (error) return error; + */ +async function requireAgency(agencyId: string, env: HandlerEnv): Promise { + const decodedId = decodeURIComponent(agencyId); + const exists = await agencyExists(decodedId, env); + if (!exists) { + return new Response( + JSON.stringify({ + error: "Agency not found", + message: `Agency '${decodedId}' does not exist. Create it first with POST /agencies`, + agencyId: decodedId, + }), + { status: 404, headers: { "content-type": "application/json" } } + ); + } + return null; } const deleteAgency = async (req: IRequest, { ctx }: RequestContext) => { @@ -190,12 +219,18 @@ const deleteBlueprint = async (req: IRequest, { ctx }: RequestContext) => { ); }; -const listAgents = async (req: IRequest, { ctx }: RequestContext) => { +const listAgents = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); return agencyStub.fetch(new Request("http://do/agents")); }; -const createAgent = async (req: IRequest, { ctx }: RequestContext) => { +const createAgent = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); const body = await req.json>(); body.requestContext = buildRequestContext(req); @@ -293,12 +328,18 @@ const getScheduleRuns = async (req: IRequest, { ctx }: RequestContext) => { // --- Vars --- -const getVars = async (req: IRequest, { ctx }: RequestContext) => { +const getVars = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); return agencyStub.fetch(new Request("http://do/vars")); }; -const setVars = async (req: IRequest, { ctx }: RequestContext) => { +const setVars = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); return agencyStub.fetch( new Request("http://do/vars", { @@ -309,12 +350,18 @@ const setVars = async (req: IRequest, { ctx }: RequestContext) => { ); }; -const getVar = async (req: IRequest, { ctx }: RequestContext) => { +const getVar = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); return agencyStub.fetch(new Request(`http://do/vars/${req.params.varKey}`)); }; -const setVar = async (req: IRequest, { ctx }: RequestContext) => { +const setVar = async (req: IRequest, { ctx, env }: RequestContext) => { + const notFound = await requireAgency(req.params.agencyId, env); + if (notFound) return notFound; + const agencyStub = await getAgencyStub(req.params.agencyId, ctx); return agencyStub.fetch( new Request(`http://do/vars/${req.params.varKey}`, {