Files
deepseek-harness/packages/agent-loop/src/index.ts
T
Tianyi Cui 7f024a1a9d Document the codebase thoroughly and tighten type safety
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.
2026-06-11 13:01:00 +08:00

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