diff --git a/bun.lock b/bun.lock index a69ff34..73251d6 100644 --- a/bun.lock +++ b/bun.lock @@ -15,9 +15,11 @@ "@biomejs/biome": "^2.4.16", "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.31.0", + "@mohanscodex/spectra-app": "^0.5.1", "turbo": "^2.9.18", "typedoc": "^0.28.19", "vitepress": "^1.6.4", + "zod": "^4.4.3", }, }, "apps/examples": { @@ -50,7 +52,7 @@ }, "packages/agent": { "name": "@mohanscodex/spectra-agent", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@mohanscodex/spectra-ai": "workspace:*", "zod": "^3.25.0", @@ -63,7 +65,7 @@ }, "packages/ai": { "name": "@mohanscodex/spectra-ai", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@anthropic-ai/sdk": "^0.32.0", "openai": "^5.3.0", @@ -75,7 +77,7 @@ }, "packages/app": { "name": "@mohanscodex/spectra-app", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@mohanscodex/spectra-agent": "workspace:*", "@mohanscodex/spectra-ai": "workspace:*", @@ -94,7 +96,7 @@ }, "packages/code": { "name": "@mohanscodex/spectra-code", - "version": "0.5.0", + "version": "0.5.1", "bin": { "spectra": "dist/src/cli.js", }, @@ -1523,7 +1525,7 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -1547,8 +1549,12 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@mohanscodex/spectra-agent/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "@mohanscodex/spectra-agent/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@mohanscodex/spectra-ai/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], "@mohanscodex/spectra-app/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], @@ -1557,6 +1563,10 @@ "@mohanscodex/spectra-code/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "@mohanscodex/spectra-code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@mohanscodex/spectra-examples/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@mohanscodex/spectra-tui/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], "@opentui/core/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], diff --git a/package.json b/package.json index bbd1257..446fa18 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,11 @@ "@biomejs/biome": "^2.4.16", "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.31.0", + "@mohanscodex/spectra-app": "workspace:*", "turbo": "^2.9.18", "typedoc": "^0.28.19", - "vitepress": "^1.6.4" + "vitepress": "^1.6.4", + "zod": "^4.4.3" }, "dependencies": { "@opentui/core-linux-x64": "0.3.4", diff --git a/packages/app/src/__tests__/app.test.ts b/packages/app/src/__tests__/app.test.ts index 27f6910..5683691 100644 --- a/packages/app/src/__tests__/app.test.ts +++ b/packages/app/src/__tests__/app.test.ts @@ -12,8 +12,10 @@ import { SequentialWorkerPool, createAgentRunner, } from '../index.js'; -import { Agent } from '@mohanscodex/spectra-agent'; -import type { Model, Message } from '@mohanscodex/spectra-ai'; +import { Agent, defineTool } from '@mohanscodex/spectra-agent'; +import type { Model, Message, AssistantMessage } from '@mohanscodex/spectra-ai'; +import { AssistantMessageEventStream, registerProvider } from '@mohanscodex/spectra-ai'; +import { z } from 'zod'; const testModel: Model = { id: 'test-model', @@ -589,8 +591,218 @@ describe('AgentRegistry', () => { expect(results[0].success).toBe(false); expect(results[1].success).toBe(false); }); + + it('should delegate to a registered agent and return result', async () => { + const providerModel: Model = { id: 'test-model', name: 'Test', provider: 'test-provider', api: 'test' }; + const provider = createMockProvider('test-provider', [[createTextMessage('Found 12 files')]]); + registerProvider(provider); + + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('explore', { + model: providerModel, + systemPrompt: 'You are an explorer.', + }); + + const result = await orchestrator.delegate('explore', 'find auth files'); + + expect(result.success).toBe(true); + expect(result.result).toBe('Found 12 files'); + expect(result.messages).toBeDefined(); + expect(result.messages!.length).toBeGreaterThan(0); + expect(result.usage).toBeDefined(); + }); + + it('should create child session with parentSessionId when sessionManager provided', async () => { + const providerModel: Model = { id: 'test-model', name: 'Test', provider: 'test-provider', api: 'test' }; + const provider = createMockProvider('test-provider', [[createTextMessage('done')]]); + registerProvider(provider); + + const store = new InMemorySessionStore(); + const manager = new SessionManager(store); + const orchestrator = new AgentRegistry(manager); + orchestrator.registerAgent('explore', { model: providerModel, systemPrompt: 'Explore.' }); + + const parentSession = await manager.create({ model: providerModel }); + + const result = await orchestrator.delegate('explore', 'test', { + parentSessionId: parentSession.id, + }); + + expect(result.success).toBe(true); + expect(result.childSessionId).toBeDefined(); + expect(result.messages!.length).toBeGreaterThan(0); + + const childSession = await manager.load(result.childSessionId!); + expect(childSession).not.toBeNull(); + expect(childSession!.metadata.parentSessionId).toBe(parentSession.id); + expect(childSession!.entries.length).toBeGreaterThan(0); + }); + + it('should use parentModel as fallback when agent has no model', async () => { + let capturedModel: Model | undefined; + const parentModel: Model = { id: 'gpt-4o', name: 'GPT-4o', provider: 'test-provider', api: 'test' }; + const provider = { + name: 'test-provider', + stream(model: Model) { + capturedModel = model; + const stream = new AssistantMessageEventStream(); + const msg = createTextMessage('ok'); + setTimeout(() => { + stream.push({ type: 'start', partial: msg }); + stream.push({ type: 'done', reason: 'stop', message: msg }); + stream.end(); + }, 10); + return stream; + }, + }; + registerProvider(provider); + + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('explore', { systemPrompt: 'Explore.' }); + + await orchestrator.delegate('explore', 'test', { parentModel }); + + expect(capturedModel?.id).toBe('gpt-4o'); + }); + + it('should use agent model as override when agent has a model', async () => { + let capturedModel: Model | undefined; + const cheapModel: Model = { id: 'deepseek-flash', name: 'DeepSeek', provider: 'test-provider', api: 'test' }; + const parentModel: Model = { id: 'gpt-4o', name: 'GPT-4o', provider: 'test-provider', api: 'test' }; + const provider = { + name: 'test-provider', + stream(model: Model) { + capturedModel = model; + const stream = new AssistantMessageEventStream(); + const msg = createTextMessage('ok'); + setTimeout(() => { + stream.push({ type: 'start', partial: msg }); + stream.push({ type: 'done', reason: 'stop', message: msg }); + stream.end(); + }, 10); + return stream; + }, + }; + registerProvider(provider); + + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('explore', { model: cheapModel, systemPrompt: 'Explore.' }); + + await orchestrator.delegate('explore', 'test', { parentModel }); + + expect(capturedModel?.id).toBe('deepseek-flash'); + }); + + it('should pass tools to child agent', async () => { + const providerModel: Model = { id: 'test-model', name: 'Test', provider: 'test-provider', api: 'test' }; + const provider = createMockProvider('test-provider', [[createTextMessage('Echoed: hi')]]); + registerProvider(provider); + + const echoTool = defineTool({ + name: 'echo', + description: 'Echo', + parameters: z.object({ text: z.string() }), + execute: async ({ text }) => ({ + content: [{ type: 'text', text: `Echo: ${text}` }], + }), + }); + + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('worker', { model: providerModel, systemPrompt: 'Worker.' }); + + const result = await orchestrator.delegate('worker', 'echo hi', { tools: [echoTool] }); + + expect(result.success).toBe(true); + expect(result.result).toBe('Echoed: hi'); + }); + + it('should wire onEvent callback to child agent events', async () => { + const providerModel: Model = { id: 'test-model', name: 'Test', provider: 'test-provider', api: 'test' }; + const provider = createMockProvider('test-provider', [[createTextMessage('ok')]]); + registerProvider(provider); + + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('worker', { model: providerModel, systemPrompt: 'Worker.' }); + + const events: string[] = []; + await orchestrator.delegate('worker', 'test', { + onEvent: (e) => events.push(e.type), + }); + + expect(events).toContain('agent_start'); + expect(events).toContain('agent_end'); + }); + + it('should return error for missing model', async () => { + const orchestrator = new AgentRegistry(); + orchestrator.registerAgent('broken', { systemPrompt: 'No model.' }); + + const result = await orchestrator.delegate('broken', 'test'); + + expect(result.success).toBe(false); + expect(result.error).toContain('No model available'); + }); }); +function createTextMessage(text: string): AssistantMessage { + return { + role: 'assistant', + content: [{ type: 'text', text }], + provider: 'test-provider', + model: 'test-model', + usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 }, + stopReason: 'stop', + timestamp: Date.now(), + }; +} + +function createMockProvider(name: string, responseSequence: AssistantMessage[][]) { + let callIndex = 0; + + return { + name, + stream(model: Model) { + const stream = new AssistantMessageEventStream(); + const responses = responseSequence[callIndex] || []; + callIndex++; + + setTimeout(() => { + const partial: AssistantMessage = { + role: 'assistant', + content: [], + provider: model.provider, + model: model.id, + usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 }, + stopReason: 'stop', + timestamp: Date.now(), + }; + + stream.push({ type: 'start', partial }); + + for (let i = 0; i < responses.length; i++) { + const msg = responses[i]; + for (const block of msg.content) { + if (block.type === 'text') { + stream.push({ + type: 'text_delta', + contentIndex: i, + delta: block.text, + partial: { ...partial, content: [block] }, + }); + } + } + } + + const lastResponse = responses[responses.length - 1] || partial; + stream.push({ type: 'done', reason: lastResponse.stopReason, message: lastResponse }); + stream.end(); + }, 10); + + return stream; + }, + }; +} + describe('WorkerPool', () => { it('should enqueue and process jobs', async () => { const store = new InMemorySessionStore(); diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index ea363f8..026c017 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -36,6 +36,7 @@ export type { RateLimiter, RateLimitResult, Orchestrator, + DelegateOptions, Budget, TaskConfig, DelegationResult, diff --git a/packages/app/src/orchestrator.ts b/packages/app/src/orchestrator.ts index 65a96da..1d4b0e7 100644 --- a/packages/app/src/orchestrator.ts +++ b/packages/app/src/orchestrator.ts @@ -1,14 +1,25 @@ -import { Agent } from '@mohanscodex/spectra-agent'; -import type { Budget, DelegationResult, TaskConfig } from './types.js'; +import { runSubagent } from '@mohanscodex/spectra-agent'; +import type { AgentTool } from '@mohanscodex/spectra-agent'; +import type { Model } from '@mohanscodex/spectra-ai'; +import type { Budget, DelegateOptions, DelegationResult, TaskConfig } from './types.js'; +import type { SessionManager } from './session-manager.js'; + +interface AgentRegistryEntry { + model?: Model; + systemPrompt?: string; + tools?: AgentTool[]; +} export class AgentRegistry { - private agents: Map = new Map(); + private agents: Map = new Map(); - registerAgent(agentType: string, config: any): void { + constructor(private sessionManager?: SessionManager) {} + + registerAgent(agentType: string, config: AgentRegistryEntry): void { this.agents.set(agentType, config); } - async delegate(agentType: string, task: string, budget?: Budget): Promise { + async delegate(agentType: string, task: string, opts?: DelegateOptions): Promise { const agentConfig = this.agents.get(agentType); if (!agentConfig) { return { @@ -19,30 +30,52 @@ export class AgentRegistry { }; } + const resolvedModel = opts?.parentModel ?? agentConfig.model; + if (!resolvedModel) { + return { + agentType, + success: false, + result: '', + error: `No model available: agent "${agentType}" has no model and no parentModel provided`, + }; + } + try { - const agent = new Agent({ - model: agentConfig.model, - systemPrompt: agentConfig.systemPrompt, - tools: agentConfig.tools, - }); - - const events: any[] = []; - for await (const event of agent.run(task)) { - events.push(event); - } + const result = await runSubagent( + { + model: resolvedModel, + modelOverride: agentConfig.model, + systemPrompt: agentConfig.systemPrompt, + tools: opts?.tools ?? agentConfig.tools, + budget: opts?.budget, + signal: opts?.signal, + onEvent: opts?.onEvent, + }, + task, + ); + + let childSessionId: string | undefined; - // Get final assistant message - const assistantMessage = agent.messages.find((m: any) => m.role === 'assistant'); - let result = 'No response'; - if (assistantMessage?.content?.[0]) { - const content = assistantMessage.content[0]; - result = typeof content === 'string' ? content : ((content as any).text ?? 'No response'); + if (this.sessionManager) { + const childSession = await this.sessionManager.create({ model: resolvedModel }); + if (opts?.parentSessionId) { + childSession.metadata.parentSessionId = opts.parentSessionId; + } + for (const msg of result.messages) { + this.sessionManager.appendMessage(childSession, msg); + } + await this.sessionManager.save(childSession); + childSessionId = childSession.id; } return { agentType, - success: true, - result, + success: !result.error && !result.aborted, + result: result.text, + messages: result.messages, + childSessionId, + usage: result.usage, + error: result.error, }; } catch (err) { return { @@ -54,9 +87,17 @@ export class AgentRegistry { } } - async executeParallel(tasks: TaskConfig[]): Promise { - const promises = tasks.map((task) => this.delegate(task.agentType, task.task, task.budget)); - + async executeParallel( + tasks: TaskConfig[], + opts?: Pick, + ): Promise { + const promises = tasks.map((task) => + this.delegate(task.agentType, task.task, { + ...opts, + tools: task.tools, + budget: task.budget, + }), + ); return Promise.all(promises); } } diff --git a/packages/app/src/types.ts b/packages/app/src/types.ts index 40860d2..5309c8c 100644 --- a/packages/app/src/types.ts +++ b/packages/app/src/types.ts @@ -136,9 +136,18 @@ export interface RateLimitResult { resetAt: Date; } +export interface DelegateOptions { + parentModel?: Model; + parentSessionId?: string; + signal?: AbortSignal; + onEvent?: (event: AgentEvent) => void; + tools?: AgentTool[]; + budget?: Budget; +} + export interface Orchestrator { - delegate(agentType: string, task: string, budget?: Budget): Promise; - executeParallel(tasks: TaskConfig[]): Promise; + delegate(agentType: string, task: string, opts?: DelegateOptions): Promise; + executeParallel(tasks: TaskConfig[], opts?: Pick): Promise; } export interface Budget { @@ -150,6 +159,7 @@ export interface Budget { export interface TaskConfig { agentType: string; task: string; + tools?: AgentTool[]; budget?: Budget; } @@ -157,6 +167,8 @@ export interface DelegationResult { agentType: string; success: boolean; result: string; + messages?: Message[]; + childSessionId?: string; usage?: Usage; error?: string; }