Add agent response scheduler (#158)

* feat: add agent response scheduler

* feat: sendMessage rpc returns created entry

* feat: make agent event include conversation entry

* feat: add agent response logging
This commit is contained in:
2026-07-04 17:35:50 +01:00
committed by GitHub
parent 6401616890
commit f8669c681c
35 changed files with 2769 additions and 648 deletions

View File

@@ -10,6 +10,6 @@
},
"dependencies": {
"@freya/agent-protocol": "workspace:*",
"@nym.sh/jrpc": "^0.1.0"
"@nym.sh/jrpc": "1.1.0"
}
}

View File

@@ -1,14 +1,17 @@
import type {
AgentClientApi,
AgentEvent,
AgentServerApi,
SendMessageResult,
} from "@freya/agent-protocol"
import type { JrpcChannel, JrpcMessage, JsonRpcMessage } from "@nym.sh/jrpc"
import {
AgentEventKind,
type AgentClientApi,
type AgentConversationEntryCreatedEvent,
type AgentEvent,
type AgentServerApi,
} from "@freya/agent-protocol"
import { JsonRpcClient, JsonRpcServer } from "@nym.sh/jrpc"
type JsonObject = Record<string, unknown>
type MessagePart = { type: "text"; text: string } | { type: "json"; value: unknown }
type SendMessageResult = Awaited<ReturnType<AgentServerApi["sendMessage"]>>
interface AuthUser {
id: string
@@ -71,7 +74,6 @@ class AgentWebSocketSession implements AgentClientApi {
private readonly client: JsonRpcClient<AgentServerApi>
private readonly server: JsonRpcServer<AgentClientApi>
private conversationId: string | undefined
private responseHadText = false
private constructor(channel: WebSocketJrpcChannel) {
this.channel = channel
@@ -105,36 +107,23 @@ class AgentWebSocketSession implements AgentClientApi {
}
async ask(message: string): Promise<void> {
this.responseHadText = false
const result = await this.sendMessage(message)
if (result.conversationId) {
this.conversationId = result.conversationId
}
if (!this.responseHadText) {
console.log(`\nagent> ${result.message || "(no message)"}`)
}
const entry = await this.sendMessage(message)
this.conversationId = entry.conversationId
console.log(`\nqueued> ${entry.kind} ${entry.id}`)
console.log("")
}
notify(event: AgentEvent): void {
switch (event.type) {
case "conversation_started":
switch (event.kind) {
case AgentEventKind.ConversationStarted:
this.conversationId = event.conversationId
break
case "message_created":
this.printMessage(event.text)
case AgentEventKind.ConversationEntryCreated:
this.printConversationEntry(event.entry)
break
case "tool_started":
console.log(`\ntool> ${event.toolName} started`)
case AgentEventKind.ResponseFinished:
break
case "tool_finished":
console.log(`tool> ${event.toolName} ${event.ok ? "finished" : "failed"}`)
break
case "message_finished":
break
case "message_failed":
case AgentEventKind.ResponseFailed:
console.log(`\nagent! ${event.error}`)
break
}
@@ -156,7 +145,31 @@ class AgentWebSocketSession implements AgentClientApi {
if (text === "") return
console.log(`\nagent> ${text}`)
this.responseHadText = true
}
private printConversationEntry(entry: AgentConversationEntryCreatedEvent["entry"]): void {
this.conversationId = entry.conversationId
switch (entry.kind) {
case "assistant_message":
this.printMessage(messagePartsText(entry.payload.parts))
break
case "tool_call":
console.log(`\ntool> ${payloadString(entry.payload, "toolName", "unknown")} started`)
break
case "tool_result":
console.log(
`tool> ${payloadString(entry.payload, "toolName", "unknown")} ${
payloadBoolean(entry.payload, "ok") ? "finished" : "failed"
}`,
)
break
case "user_message":
case "attachment":
case "context_summary":
case "system_note":
break
}
}
}
@@ -691,6 +704,28 @@ function parseJsonArgument(value: string, fallback: unknown): unknown {
}
}
function messagePartsText(parts: MessagePart[]): string {
return parts.map(messagePartText).join("\n")
}
function messagePartText(part: MessagePart): string {
switch (part.type) {
case "text":
return part.text
case "json":
return formatJson(part.value)
}
}
function payloadString(payload: Record<string, unknown>, key: string, fallback: string): string {
const value = payload[key]
return typeof value === "string" ? value : fallback
}
function payloadBoolean(payload: Record<string, unknown>, key: string): boolean {
return payload[key] === true
}
function formatJson(value: unknown): string {
const serialized = JSON.stringify(value, null, 2)
return serialized ?? "undefined"