11import { z } from "zod" ;
2- import { convertMessagesForChatCompletion } from "~/src/agent/message" ;
32import {
43 agentToolSchema ,
54 type AgentToolBuilder ,
65 type AgentTool ,
76} from "~/src/agent/tool" ;
8- import type { Message } from "~/src/job/schema" ;
7+ import type {
8+ Message ,
9+ ToolMessage ,
10+ MessageChunk ,
11+ AssistantMessage ,
12+ } from "~/src/job/schema" ;
913import type { ChatBuilder } from "~/src/builder/chat" ;
1014
1115export const agentSchema = z . object ( {
@@ -14,7 +18,30 @@ export const agentSchema = z.object({
1418 tools : z . array ( agentToolSchema ) ,
1519} ) ;
1620
17- interface GenerateOptions {
21+ interface ChunkEvent {
22+ type : "chunk" ;
23+ chunk : {
24+ text ?: string ;
25+ reasoning ?: string ;
26+ } ;
27+ }
28+
29+ interface ToolEvent {
30+ type : "tool" ;
31+ tool : {
32+ name : string ;
33+ args : any ;
34+ result ?: any ;
35+ error ?: any ;
36+ } ;
37+ }
38+
39+ interface MessageEvent {
40+ type : "message" ;
41+ message : Message ;
42+ }
43+
44+ export interface AgentGenerateOptions {
1845 maxSteps : number ;
1946}
2047
@@ -46,28 +73,27 @@ export class Agent<TContext = any> {
4673 generate = async function * (
4774 this : Agent < TContext > ,
4875 initialMessages : Message [ ] ,
49- options : GenerateOptions ,
76+ options : AgentGenerateOptions ,
5077 context ?: TContext ,
5178 ) {
5279 const body = agentSchema . parse ( this . body ) ;
5380
54- let shouldBreak = false ;
81+ let shouldFinish = false ;
5582 let newMessages : Message [ ] = [ ] ;
5683 for ( let iteration = 0 ; iteration < options . maxSteps ; iteration ++ ) {
57- if ( shouldBreak ) {
84+ if ( shouldFinish ) {
5885 break ;
5986 }
6087
6188 const instructions =
6289 typeof body . instructions === "function"
63- ? body . instructions ( )
90+ ? body . instructions ( ) // TODO: more context
6491 : body . instructions ;
65- const allMessages = initialMessages . concat ( newMessages ) ;
66- const convertedMessages = convertMessagesForChatCompletion ( allMessages ) ;
67- const messages = [ { role : "system" , content : instructions } ] . concat (
68- convertedMessages as any ,
92+ const systemMessage = { role : "system" , text : instructions } ;
93+ const messages = ( [ systemMessage ] as Message [ ] ) . concat (
94+ initialMessages ,
95+ newMessages ,
6996 ) ;
70- // TODO: agent tool vs chat tool
7197 const tools = body . tools . map ( ( tool ) => ( {
7298 name : tool . name ,
7399 description : tool . description ,
@@ -78,82 +104,87 @@ export class Agent<TContext = any> {
78104 . stream ( )
79105 . run ( ) ;
80106
81- let totalText = "" ;
82- for await ( const chunk of result ) {
83- const delta = chunk . raw . choices [ 0 ] . delta ;
107+ let newAssistantMessage : AssistantMessage = {
108+ role : "assistant" ,
109+ text : "" ,
110+ reasoning : "" ,
111+ } ;
84112
85- // TODO: tool calls with content??
86- if ( delta . tool_calls ) {
87- // TODO: tool call with content
88- // TODO: tool call with input streaming
89- // TODO: support multiple tool calls
90- const toolCall = delta . tool_calls [ 0 ] ;
91- const toolName = toolCall . function . name ;
92- const input = JSON . parse ( toolCall . function . arguments ) ; // TODO: parsing error handling
113+ for await ( const chunk of result as AsyncIterable < MessageChunk > ) {
114+ if ( chunk . toolCalls ) {
115+ // existing assistant message chunked out before tool call
116+ if ( newAssistantMessage . text || newAssistantMessage . reasoning ) {
117+ yield {
118+ type : "message" ,
119+ message : newAssistantMessage ,
120+ } as MessageEvent ;
121+ newMessages . push ( newAssistantMessage ) ;
122+ newAssistantMessage = {
123+ role : "assistant" ,
124+ text : "" ,
125+ reasoning : "" ,
126+ } ;
127+ }
93128
94- const agentTool = body . tools . find ( ( t ) => t . name === toolName ) ;
129+ const toolCall = chunk . toolCalls [ 0 ] ;
130+ const { name, arguments : args } = toolCall . function ;
131+ const agentTool = body . tools . find ( ( t ) => t . name === name ) ;
95132 if ( ! agentTool ) {
96- throw new Error ( `Unknown tool: ${ toolName } ` ) ;
133+ throw new Error ( `Unknown tool: ${ name } ` ) ;
97134 }
98135
99- const toolPart = {
100- type : "tool-" + toolName ,
101- toolCallId : toolCall . id ,
102- input : input ,
103- } ;
104-
105- yield { type : "tool-call-input" , data : toolPart } ;
106-
107- let output = null ;
108- let outputError = null ;
136+ yield { type : "tool" , tool : { name, args } } ;
109137
138+ let result = null ;
139+ let error = null ;
110140 try {
111- output = await agentTool . execute ( input , context ! ) ;
141+ result = await agentTool . execute ( args , context ! ) ;
112142 } catch ( err ) {
113- outputError = ( err as Error ) . message ;
143+ error = ( err as Error ) . message ;
114144 }
115145
116- if ( outputError ) {
117- yield {
118- type : "tool-call-output" ,
119- data : { ...toolPart , outputError } ,
120- } ;
121- } else {
122- yield { type : "tool-call-output" , data : { ...toolPart , output } } ;
123- }
146+ yield {
147+ type : "tool" ,
148+ tool : { name, args, result, error } ,
149+ } as ToolEvent ;
124150
125- const newMessage : Message = {
151+ const newMessage : ToolMessage = {
126152 role : "tool" ,
127- parts : [
128- {
129- type : `tool-${ toolName } ` ,
130- toolCallId : toolCall . id ,
131- input : input ,
132- output : output ,
133- outputError : outputError ,
134- } ,
135- ] ,
153+ text : "" ,
154+ content : {
155+ callId : toolCall . id ,
156+ name : name ,
157+ args : args ,
158+ result : result ,
159+ error : error ,
160+ } ,
136161 } ;
137162
138- yield { type : "message-created " , data : newMessage } ;
163+ yield { type : "message" , message : newMessage } as MessageEvent ;
139164 newMessages . push ( newMessage ) ;
140- } else if ( delta . content ) {
141- const text = delta . content as string ;
142- yield { type : "text-delta" , data : { text } } ;
143- totalText += text ;
144- shouldBreak = true ;
165+ shouldFinish = false ;
166+ } else if ( chunk . text || chunk . reasoning ) {
167+ yield {
168+ type : "chunk" ,
169+ chunk : {
170+ text : chunk . text ,
171+ reasoning : chunk . reasoning ,
172+ } ,
173+ } as ChunkEvent ;
174+
175+ if ( chunk . text ) {
176+ newAssistantMessage . text += chunk . text ;
177+ }
178+ if ( chunk . reasoning ) {
179+ newAssistantMessage . reasoning += chunk . reasoning ;
180+ }
181+ shouldFinish = true ;
145182 }
146183 }
147184
148- if ( totalText . trim ( ) ) {
149- const newMessage : Message = {
150- role : "assistant" ,
151- parts : [ { type : "text" , text : totalText . trim ( ) } ] ,
152- } ;
153-
154- yield { type : "message-created" , data : newMessage } ;
155- newMessages . push ( newMessage ) ;
156- shouldBreak = true ;
185+ if ( newAssistantMessage . text || newAssistantMessage . reasoning ) {
186+ yield { type : "message" , message : newAssistantMessage } as MessageEvent ;
187+ newMessages . push ( newAssistantMessage ) ;
157188 }
158189 }
159190 } ;
0 commit comments