Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export interface SpawnAgentRequest {
agentType: string;
relatedAgentId?: string;
input?: Record<string, unknown>;
/** 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 {
Expand Down Expand Up @@ -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<T>(
Expand Down
48 changes: 45 additions & 3 deletions lib/runtime/agency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,9 +731,10 @@ export class Agency extends Agent<AgentEnv> {
requestContext?: ThreadRequestContext;
input?: Record<string, unknown>;
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<Response> {
Expand Down Expand Up @@ -868,10 +869,51 @@ export class Agency extends Agent<AgentEnv> {
agentType: string,
requestContext?: ThreadRequestContext,
input?: Record<string, unknown>,
relatedAgentId?: string
relatedAgentId?: string,
providedId?: string
): Promise<Response> {
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,
Expand Down
9 changes: 5 additions & 4 deletions lib/runtime/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]);
Expand Down Expand Up @@ -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"),
]);
Expand Down
61 changes: 54 additions & 7 deletions lib/runtime/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,36 @@ const createAgency = async (req: IRequest, { env }: RequestContext) => {


async function getAgencyStub(agencyId: string, ctx: CfCtx): Promise<DurableObjectStub<Agency>> {
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<boolean> {
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<Response | null> {
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) => {
Expand Down Expand Up @@ -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<Record<string, unknown>>();
body.requestContext = buildRequestContext(req);
Expand Down Expand Up @@ -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", {
Expand All @@ -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}`, {
Expand Down