7f024a1a9d
Docs: per-folder README.md for packages/ (family overview + one per package: service, events, API, extension points, TODOs), examples/, and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md symlinks) for packages/ and vendor/; module-level doc comments in every packages/*/src file; richer JSDoc on all exported API (event side effects, disposal contracts, error behavior). Root AGENTS.md gains a "Type Safety and Documentation" policy section: the codebase aims to be very type-safe and well documented; type gymnastics are acceptable in core packages when they improve plugin-author DX; verbose docs are fine as long as they stay strictly in sync with the code. Type safety: removed the upstream-inherited "noImplicitAny": false from tsconfig.base.json — packages/* now compile under full strict mode; vendor/loader and vendor/include set it locally (vendor/cordis already did). Eliminated every `: any` / `as any` from packages and examples (catch clauses use unknown + a CodedError narrowing type; event data access uses discriminated-union narrowing). Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL — SchemaSpec with per-property `required: true` booleans, type-level InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and defineTool() so first-party tools get typed execute(args) with zero casts (raw JSON Schema still accepted for MCP interop; chosen over schemastery because it targets JSON Schema generation directly). echo-tool and all test tools migrated; +7 tests.
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
/**
|
|
* THE concrete agent plugin: creates LoopAgents, runs their loops, and
|
|
* registers them in ctx.agents. Deliberately thin — every behavior beyond
|
|
* "call the model, run the tools, repeat" belongs to plugins on the event
|
|
* taxonomy.
|
|
*
|
|
* @module @deepseek-ai/dsh-agent-loop
|
|
*/
|
|
|
|
import { Context, Service } from 'cordis'
|
|
import z from 'schemastery'
|
|
import type { AgentOptions } from '@deepseek-ai/dsh-agent'
|
|
import type {} from '@deepseek-ai/dsh-llm'
|
|
import type {} from '@deepseek-ai/dsh-session'
|
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
import { LoopAgent } from './agent.ts'
|
|
|
|
export { LoopAgent } from './agent.ts'
|
|
export { Inbox, type InboxMessage } from './inbox.ts'
|
|
export { runLoop } from './loop.ts'
|
|
|
|
declare module 'cordis' {
|
|
interface Context {
|
|
agentLoop: AgentLoop
|
|
}
|
|
}
|
|
|
|
export interface Config {
|
|
/** Agents created from configuration at startup. */
|
|
agents: (AgentOptions & { id: string })[]
|
|
}
|
|
|
|
/**
|
|
* The agent-loop plugin (`ctx.agentLoop`): creates {@link LoopAgent}s, runs
|
|
* their loops, and registers them in `ctx.agents`.
|
|
*
|
|
* The loop itself is deliberately thin — every behavior beyond "call the
|
|
* model, run the tools, repeat" belongs to plugins listening on the event
|
|
* taxonomy declared in @deepseek-ai/dsh-agent.
|
|
*/
|
|
export class AgentLoop extends Service {
|
|
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
|
|
|
|
static Config: z<Config> = z.object({
|
|
agents: z.array(z.object({
|
|
id: z.string().required(),
|
|
model: z.string(),
|
|
systemPrompt: z.string(),
|
|
})).default([]),
|
|
})
|
|
|
|
constructor(ctx: Context, public config: Config) {
|
|
super(ctx, 'agentLoop')
|
|
for (const { id, ...options } of config.agents) {
|
|
this.create(id, options)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an agent, start its loop, and register it. Returns the agent.
|
|
* Disposed with the calling fiber.
|
|
*
|
|
* TODO(sub-agents): spawn/fork land here — accept a parent agent reference;
|
|
* fork seeds the new Session with the parent's event log, spawn starts
|
|
* fresh; the child is returned as a regular Agent handle.
|
|
*/
|
|
create(id: string, options: AgentOptions = {}): LoopAgent {
|
|
const session = this.ctx.sessions.create(`${id}-session`)
|
|
const agent = new LoopAgent(this.ctx, id, options, session)
|
|
// Generator effect: stop and unregister are independent disposables
|
|
// (LIFO), so a throwing stop() cannot leak the registry entry.
|
|
this.ctx.effect(function* (this: AgentLoop) {
|
|
yield this.ctx.agents.register(agent)
|
|
yield agent.start()
|
|
}.bind(this), 'agentLoop.create()')
|
|
return agent
|
|
}
|
|
}
|
|
|
|
export default AgentLoop
|