88 * Supports four LLM providers: anthropic, google, openai, workers-ai.
99 */
1010
11- import { DurableObject } from "cloudflare:workers" ;
1211import Anthropic from "@anthropic-ai/sdk" ;
1312import type { ToolDefinition , ToolReference } from "@dafthunk/runtime" ;
1413import { NodeToolProvider } from "@dafthunk/runtime" ;
1514import type { AgentProvider } from "@dafthunk/runtime/nodes/agent/base-agent-node" ;
1615import type {
1716 AgentLoopResult ,
17+ AgentLoopState ,
1818 AgentMessage ,
1919 LLMResponse ,
2020} from "@dafthunk/runtime/utils/agent-loop" ;
@@ -28,6 +28,7 @@ import { createCodeModeToolDefinition } from "@dafthunk/runtime/utils/code-mode"
2828import type { TokenPricing } from "@dafthunk/runtime/utils/usage" ;
2929import { calculateTokenUsage } from "@dafthunk/runtime/utils/usage" ;
3030import { GoogleGenAI } from "@google/genai" ;
31+ import { Agent } from "agents" ;
3132import OpenAI from "openai" ;
3233
3334import type { Bindings } from "../context" ;
@@ -69,6 +70,8 @@ export interface AgentRunRequest {
6970 googleSearch ?: boolean ;
7071 /** Organization ID for credential access (integrations, secrets) */
7172 organizationId : string ;
73+ /** When set, routes to a persistent DO that maintains state across runs */
74+ agentId ?: string ;
7275}
7376
7477export interface AgentRunResponse {
@@ -78,11 +81,21 @@ export interface AgentRunResponse {
7881 totalSteps : number ;
7982 totalInputTokens : number ;
8083 totalOutputTokens : number ;
84+ /** Full message history (only present when agentId is set) */
85+ agentMessages ?: AgentMessage [ ] ;
86+ }
87+
88+ // ── Persistent state for stateful conversations ─────────────────────────
89+
90+ export interface AgentRunnerState {
91+ messages : AgentMessage [ ] ;
92+ totalInputTokens : number ;
93+ totalOutputTokens : number ;
8194}
8295
8396// ── Durable Object ───────────────────────────────────────────────────────
8497
85- export class AgentRunner extends DurableObject < Bindings > {
98+ export class AgentRunner extends Agent < Bindings , AgentRunnerState > {
8699 private initialized = false ;
87100
88101 constructor ( ctx : DurableObjectState , env : Bindings ) {
@@ -199,6 +212,9 @@ export class AgentRunner extends DurableObject<Bindings> {
199212 // Build built-in Gemini tools (only effective for google provider)
200213 const geminiBuiltInTools = this . buildGeminiBuiltInTools ( body ) ;
201214
215+ // Build resume state from persisted conversation (if stateful)
216+ const resumeState = this . buildResumeState ( body . agentId , userMessage ) ;
217+
202218 // Run the agent loop
203219 const result = await runAgentLoop ( {
204220 userMessage,
@@ -220,15 +236,24 @@ export class AgentRunner extends DurableObject<Bindings> {
220236 runId
221237 ) ;
222238 } ,
239+ resumeState,
223240 } ) ;
224241
242+ // Persist conversation state for stateful sessions
243+ const agentMessages = this . persistConversationState (
244+ body . agentId ,
245+ userMessage ,
246+ result
247+ ) ;
248+
225249 const response : AgentRunResponse = {
226250 text : result . text ,
227251 steps : result . steps ,
228252 finishReason : result . finishReason ,
229253 totalSteps : result . totalSteps ,
230254 totalInputTokens : result . totalInputTokens ,
231255 totalOutputTokens : result . totalOutputTokens ,
256+ ...( agentMessages && { agentMessages } ) ,
232257 } ;
233258
234259 // Cache the completed result
@@ -350,6 +375,9 @@ export class AgentRunner extends DurableObject<Bindings> {
350375
351376 const geminiBuiltInTools = this . buildGeminiBuiltInTools ( body ) ;
352377
378+ // Build resume state from persisted conversation (if stateful)
379+ const resumeState = this . buildResumeState ( body . agentId , userMessage ) ;
380+
353381 const result = await runAgentLoop ( {
354382 userMessage,
355383 tools : toolDefinitions ,
@@ -370,15 +398,24 @@ export class AgentRunner extends DurableObject<Bindings> {
370398 runId
371399 ) ;
372400 } ,
401+ resumeState,
373402 } ) ;
374403
404+ // Persist conversation state for stateful sessions
405+ const agentMessages = this . persistConversationState (
406+ body . agentId ,
407+ userMessage ,
408+ result
409+ ) ;
410+
375411 const response : AgentRunResponse = {
376412 text : result . text ,
377413 steps : result . steps ,
378414 finishReason : result . finishReason ,
379415 totalSteps : result . totalSteps ,
380416 totalInputTokens : result . totalInputTokens ,
381417 totalOutputTokens : result . totalOutputTokens ,
418+ ...( agentMessages && { agentMessages } ) ,
382419 } ;
383420
384421 // Cache the completed result
@@ -456,12 +493,70 @@ export class AgentRunner extends DurableObject<Bindings> {
456493 totalInputTokens : response . totalInputTokens ,
457494 totalOutputTokens : response . totalOutputTokens ,
458495 } ,
496+ ...( response . agentMessages && {
497+ agent_messages : response . agentMessages ,
498+ } ) ,
459499 } ,
460500 usage,
461501 } ,
462502 } ) ;
463503 }
464504
505+ // ── Conversation state helpers ───────────────────────────────────────
506+
507+ /**
508+ * If a agentId is set and previous messages exist, builds a
509+ * resumeState so the agent loop continues from the prior conversation.
510+ */
511+ private buildResumeState (
512+ agentId : string | undefined ,
513+ userMessage : string
514+ ) : AgentLoopState | undefined {
515+ if ( ! agentId ) return undefined ;
516+ const prev = this . state ?. messages ;
517+ if ( ! prev || prev . length === 0 ) return undefined ;
518+
519+ return {
520+ messages : [ ...prev , { role : "user" as const , content : userMessage } ] ,
521+ steps : [ ] ,
522+ totalInputTokens : 0 ,
523+ totalOutputTokens : 0 ,
524+ } ;
525+ }
526+
527+ /**
528+ * After an agent run completes, persists the full conversation history
529+ * when agentId is set. Returns the conversation messages for
530+ * inclusion in the response, or undefined for ephemeral runs.
531+ */
532+ private persistConversationState (
533+ agentId : string | undefined ,
534+ userMessage : string ,
535+ result : AgentLoopResult
536+ ) : AgentMessage [ ] | undefined {
537+ if ( ! agentId ) return undefined ;
538+
539+ const prevMessages = this . state ?. messages ?? [ ] ;
540+ const newMessages : AgentMessage [ ] = [
541+ { role : "user" , content : userMessage } ,
542+ ] ;
543+ for ( const step of result . steps ) {
544+ newMessages . push ( step . assistantMessage ) ;
545+ newMessages . push ( ...step . toolResults ) ;
546+ }
547+ newMessages . push ( { role : "assistant" , content : result . text } ) ;
548+
549+ const allMessages = [ ...prevMessages , ...newMessages ] ;
550+ this . setState ( {
551+ messages : allMessages ,
552+ totalInputTokens :
553+ ( this . state ?. totalInputTokens ?? 0 ) + result . totalInputTokens ,
554+ totalOutputTokens :
555+ ( this . state ?. totalOutputTokens ?? 0 ) + result . totalOutputTokens ,
556+ } ) ;
557+ return allMessages ;
558+ }
559+
465560 // ── Built-in Gemini tools ─────────────────────────────────────────────
466561
467562 private buildGeminiBuiltInTools (
0 commit comments