Skip to content
Draft
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
30 changes: 15 additions & 15 deletions packages/agents-runtime/src/tools/context-tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from 'zod'
import { Type } from '@sinclair/typebox'
import type { AgentTool } from '../types'

export interface ContextToolsContext {
Expand All @@ -25,10 +25,10 @@ export function createContextTools(ctx: ContextToolsContext): Array<AgentTool> {
name: `load_timeline_range`,
label: `Load Timeline Range`,
description: `Load the rendered messages for a dropped timeline offset range.`,
parameters: z.object({
from: z.number(),
to: z.number(),
}) as unknown as AgentTool[`parameters`],
parameters: Type.Object({
from: Type.Number(),
to: Type.Number(),
}) as AgentTool[`parameters`],
execute: async (_toolCallId, params) =>
textResult(
await ctx.loadTimelineRange(
Expand All @@ -43,12 +43,12 @@ export function createContextTools(ctx: ContextToolsContext): Array<AgentTool> {
name: `load_source_range`,
label: `Load Source Range`,
description: `Load a character range from a truncated source snapshot.`,
parameters: z.object({
name: z.string(),
from: z.number(),
to: z.number(),
snapshot: z.string(),
}) as unknown as AgentTool[`parameters`],
parameters: Type.Object({
name: Type.String(),
from: Type.Number(),
to: Type.Number(),
snapshot: Type.String(),
}) as AgentTool[`parameters`],
execute: async (_toolCallId, params) =>
textResult(
await ctx.loadSourceRange(
Expand All @@ -65,10 +65,10 @@ export function createContextTools(ctx: ContextToolsContext): Array<AgentTool> {
name: `load_context_history`,
label: `Load Context History`,
description: `Load a tombstoned context entry by its original offset.`,
parameters: z.object({
id: z.string(),
offset: z.string(),
}) as unknown as AgentTool[`parameters`],
parameters: Type.Object({
id: Type.String(),
offset: Type.String(),
}) as AgentTool[`parameters`],
execute: async (_toolCallId, params) =>
textResult(
await ctx.loadContextHistory(
Expand Down
9 changes: 9 additions & 0 deletions packages/agents-runtime/test/context-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ function firstText(result: {
}

describe(`context tools`, () => {
it(`uses OpenAI-compatible object schemas with properties`, () => {
for (const tool of createContextTools(makeCtx())) {
expect(tool.parameters).toMatchObject({
type: `object`,
properties: expect.any(Object),
})
}
})

it(`load_timeline_range returns the expected payload`, async () => {
const tool = createContextTools(makeCtx()).find(
(candidate) => candidate.name === `load_timeline_range`
Expand Down
63 changes: 56 additions & 7 deletions packages/agents/src/agents/horton.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import Anthropic from '@anthropic-ai/sdk'
import { z } from 'zod'
import { serverLog } from '../log'
import { createHortonDocsSupport } from '../docs/knowledge-base'
import { createSkillTools } from '../skills/tools'
import { createSpawnWorkerTool } from '../tools/spawn-worker'
import {
modelChoiceValues,
REASONING_EFFORT_VALUES,
resolveBuiltinModelConfig,
type BuiltinModelCatalog,
} from '../model-catalog'
import {
createPromptCoderTool,
createSpawnCoderTool,
Expand Down Expand Up @@ -151,7 +158,13 @@ export async function generateTitle(

export function buildHortonSystemPrompt(
workingDirectory: string,
opts: { hasDocsSupport?: boolean; hasSkills?: boolean; docsUrl?: string } = {}
opts: {
hasDocsSupport?: boolean
hasSkills?: boolean
docsUrl?: string
modelProvider?: string
modelId?: string
} = {}
): string {
const docsTools = opts.hasDocsSupport
? `\n- search_durable_agents_docs: hybrid search over the built-in Durable Agents docs index`
Expand Down Expand Up @@ -202,6 +215,11 @@ Don't force onboarding. If someone just wants to chat or code, let them. When in
- The docs site covers: Usage (entity definition, handlers, tools, state, spawning, coordination, waking, shared state, client integration, app setup), Reference (handler context, entity definitions, configurations, tools, state proxies, wake events, registries), Entities (Horton, Worker), and Patterns (Manager-Worker, Pipeline, Map-Reduce, Dispatcher, Blackboard, Reactive Observers).
- For general coding questions unrelated to Electric Agents, use brave_search or your own knowledge.`
: ``
const modelGuidance =
opts.modelProvider && opts.modelId
? `\n# Runtime model
You are currently running via provider "${opts.modelProvider}" with model "${opts.modelId}". If the user asks what model or provider you are using, answer with these exact runtime values. Do not infer your model identity from training data or from the name of another coding tool.`
: ``
return `You are Horton, a friendly and capable assistant. You can chat, research the web, read and edit code, run shell commands, and dispatch subagents (workers) for isolated subtasks. Be warm and engaging in conversation; be precise and concrete when working with code.

# Greetings
Expand All @@ -223,7 +241,7 @@ ${docsTools}${skillsTools}
- Prefer edit over write when modifying existing files.
- You must read a file before you can edit it.
- Use absolute paths or paths relative to the current working directory.
${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}
${modelGuidance}${docsGuidance}${skillsGuidance}${onboardingGuidance}${docsUrlGuidance}

# Risky actions
Pause and confirm with the user before:
Expand Down Expand Up @@ -262,7 +280,10 @@ export function createHortonTools(
workingDirectory: string,
ctx: HandlerContext,
readSet: Set<string>,
opts: { docsSearchTool?: AgentTool } = {}
opts: {
docsSearchTool?: AgentTool
modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>
} = {}
): Array<AgentTool> {
return [
createBashTool(workingDirectory),
Expand All @@ -271,7 +292,7 @@ export function createHortonTools(
createEditTool(workingDirectory, readSet),
braveSearchTool,
fetchUrlTool,
createSpawnWorkerTool(ctx),
createSpawnWorkerTool(ctx, opts.modelConfig),
createSpawnCoderTool(ctx),
createPromptCoderTool(ctx),
...(opts.docsSearchTool ? [opts.docsSearchTool] : []),
Expand Down Expand Up @@ -302,6 +323,7 @@ function createAssistantHandler(options: {
docsSupport: HortonDocsSupport | null
docsSearchTool?: AgentTool
skillsRegistry: SkillsRegistry | null
modelCatalog: BuiltinModelCatalog
docsUrl?: string
}) {
const {
Expand All @@ -310,6 +332,7 @@ function createAssistantHandler(options: {
docsSupport,
docsSearchTool,
skillsRegistry,
modelCatalog,
docsUrl,
} = options
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0)
Expand All @@ -319,9 +342,13 @@ function createAssistantHandler(options: {
wake: WakeEvent
): Promise<void> {
const readSet = new Set<string>()
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args)
const tools = [
...ctx.electricTools,
...createHortonTools(workingDirectory, ctx, readSet, { docsSearchTool }),
...createHortonTools(workingDirectory, ctx, readSet, {
docsSearchTool,
modelConfig,
}),
...(skillsRegistry && skillsRegistry.catalog.size > 0
? createSkillTools(skillsRegistry, ctx)
: []),
Expand Down Expand Up @@ -383,8 +410,10 @@ function createAssistantHandler(options: {
hasDocsSupport: Boolean(docsSupport),
hasSkills,
docsUrl,
modelProvider: modelConfig.provider,
modelId: String(modelConfig.model),
}),
model: HORTON_MODEL,
...modelConfig,
tools,
...(streamFn && { streamFn }),
})
Expand Down Expand Up @@ -422,10 +451,16 @@ export function registerHorton(
workingDirectory: string
streamFn?: StreamFn
skillsRegistry?: SkillsRegistry | null
modelCatalog: BuiltinModelCatalog
docsUrl?: string
}
): Array<string> {
const { workingDirectory, streamFn, skillsRegistry = null } = options
const {
workingDirectory,
streamFn,
skillsRegistry = null,
modelCatalog,
} = options
const docsUrl = options.docsUrl ?? process.env.HORTON_DOCS_URL

if (process.env.BRAVE_SEARCH_API_KEY) {
Expand All @@ -451,11 +486,25 @@ export function registerHorton(
docsSupport,
docsSearchTool,
skillsRegistry,
modelCatalog,
docsUrl,
})

const hortonCreationSchema = z.object({
model: z
.enum(modelChoiceValues(modelCatalog))
.default(modelCatalog.defaultChoice.value),
reasoningEffort: z
.enum(REASONING_EFFORT_VALUES)
.default(`auto`)
.describe(
`Reasoning effort for compatible reasoning models. Auto uses a safe provider default.`
),
})

registry.define(`horton`, {
description: `Friendly capable assistant — chat, code, research, dispatch`,
creationSchema: hortonCreationSchema,
handler: assistantHandler,
})

Expand Down
40 changes: 36 additions & 4 deletions packages/agents/src/agents/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
createWriteTool,
} from '@electric-ax/agents-runtime/tools'
import { WORKER_TOOL_NAMES, createSpawnWorkerTool } from '../tools/spawn-worker'
import { HORTON_MODEL } from './horton'
import {
REASONING_EFFORT_VALUES,
resolveBuiltinModelConfig,
type BuiltinModelCatalog,
} from '../model-catalog'
import type { WorkerToolName } from '../tools/spawn-worker'
import type { AgentTool, StreamFn } from '@mariozechner/pi-agent-core'
import type {
Expand All @@ -25,6 +29,9 @@ interface WorkerArgs {
tools: Array<WorkerToolName>
sharedDb?: { id: string; schema: SharedStateSchemaMap }
sharedDbToolMode?: `full` | `write-only`
model?: string
provider?: string
reasoningEffort?: string
}

function isWorkerToolName(value: unknown): value is WorkerToolName {
Expand Down Expand Up @@ -84,6 +91,23 @@ function parseWorkerArgs(value: Readonly<Record<string, unknown>>): WorkerArgs {
throw new Error(`[worker] must provide tools and/or sharedDb`)
}

if (typeof value.model === `string`) {
args.model = value.model
}

if (typeof value.provider === `string`) {
args.provider = value.provider
}

if (
typeof value.reasoningEffort === `string` &&
(REASONING_EFFORT_VALUES as ReadonlyArray<string>).includes(
value.reasoningEffort
)
) {
args.reasoningEffort = value.reasoningEffort
}

return args
}

Expand Down Expand Up @@ -254,9 +278,13 @@ function buildSharedStateTools(

export function registerWorker(
registry: EntityRegistry,
options: { workingDirectory: string; streamFn?: StreamFn }
options: {
workingDirectory: string
streamFn?: StreamFn
modelCatalog: BuiltinModelCatalog
}
): void {
const { workingDirectory, streamFn } = options
const { workingDirectory, streamFn, modelCatalog } = options
registry.define(`worker`, {
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
async handler(ctx) {
Expand All @@ -268,6 +296,10 @@ export function registerWorker(
ctx,
readSet
)
const modelConfig = resolveBuiltinModelConfig(
modelCatalog,
args as unknown as Readonly<Record<string, unknown>>
)

const sharedStateTools: Array<AgentTool> = []
if (args.sharedDb) {
Expand All @@ -285,7 +317,7 @@ export function registerWorker(

ctx.useAgent({
systemPrompt: `${args.systemPrompt}${WORKER_PROMPT_FOOTER}`,
model: HORTON_MODEL,
...modelConfig,
tools: [...builtinTools, ...sharedStateTools],
...(streamFn && { streamFn }),
})
Expand Down
12 changes: 9 additions & 3 deletions packages/agents/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { serverLog } from './log'
import { registerCodingSession } from './agents/coding-session'
import { registerHorton } from './agents/horton'
import { registerWorker } from './agents/worker'
import { createBuiltinModelCatalog } from './model-catalog'
import { createSkillsRegistry } from './skills/registry'
import type {
AgentTool,
Expand Down Expand Up @@ -76,9 +77,13 @@ export async function createBuiltinAgentHandler(
createElectricTools,
} = options

if (!streamFn && !process.env.ANTHROPIC_API_KEY) {
const modelCatalog = await createBuiltinModelCatalog({
allowMockFallback: Boolean(streamFn),
})

if (!modelCatalog) {
serverLog.warn(
`[builtin-agents] ANTHROPIC_API_KEY not set — skipping built-in agent registration`
`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`
)
return null
}
Expand Down Expand Up @@ -111,9 +116,10 @@ export async function createBuiltinAgentHandler(
workingDirectory: cwd,
streamFn,
skillsRegistry,
modelCatalog,
})

registerWorker(registry, { workingDirectory: cwd, streamFn })
registerWorker(registry, { workingDirectory: cwd, streamFn, modelCatalog })
typeNames.push(`worker`)

registerCodingSession(registry, { defaultWorkingDirectory: cwd })
Expand Down
Loading
Loading