feat(core): add agent execution context

This commit is contained in:
Yichen Jiang
2026-07-16 16:29:46 +08:00
parent 04df615dd6
commit 7bcae0cd64
93 changed files with 1272 additions and 462 deletions

View File

@@ -9,10 +9,11 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co
| `system-prompt/` | Prompt-section + tool-schema assembly registry | `ctx.systemPrompt` |
| `tools/` | Scoped tool registry + pre-policy, guards, around-dispatch, post-policy, and final-result observation | `ctx.tools` |
| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
| `agent-execution/` | Process-local ambient Agent identity for asynchronous driver work | `ctx.agentExecution` |
| `agent-loop/` | The concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
`scope/` is the one non-service package here: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) the registries and the loop build per-agent scoping on — it sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle.
`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable.
`agent-execution` is mandatory control infrastructure shared by concrete loops and deep process-local consumers. `agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; other plugins depend on the `agent` vocabulary and execution service, never on `agent-loop` directly, so the loop stays swappable.
The default composition that wires this spine into a runnable agent lives in [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md): one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + invariants + the local [skill family](../skill/README.md) + `tool-bash` + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. It sits in `examples/` — ready-to-run demo/reference bundles — not in `core/`: `core/` ships the swappable spine pieces, while a demo bundle picks one concrete composition of them and adds a front door.
The default composition that wires this spine into a runnable agent lives in [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md): one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + agent-execution + invariants + the local [skill family](../skill/README.md) + `tool-bash` + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. It sits in `examples/` — ready-to-run demo/reference bundles — not in `core/`: `core/` ships the swappable spine pieces, while a demo bundle picks one concrete composition of them and adds a front door.

View File

@@ -0,0 +1,23 @@
# dsh-agent-execution
Process-local ambient Agent identity for asynchronous work initiated by a concrete agent driver. The default export, `AgentExecutionProvider`, installs the mandatory `ctx.agentExecution` service; [`dsh-agent-loop`](../agent-loop/README.md) establishes one boundary around each driver's complete lifetime.
## Service: `AgentExecutionService` (ctx key: `agentExecution`)
- `current()` returns the inherited `AgentExecution` or `undefined` outside a driver and inside an explicit clearing boundary.
- `require()` returns the inherited execution or throws `no agent execution context is active`.
- `run(execution, operation)` returns the exact synchronous value or Promise from `operation`. Passing `undefined` establishes a real boundary that hides an inherited Agent.
The store contains only `{ readonly agent: Agent }`. A Session is available through `agent.session`; turn, step, signal, cwd, sandbox, authorization, and other capability state remain with their explicit owners. Ambient presence identifies the initiator but does not prove that the Agent is live or that an operation is authorized.
## Lifetime and detached work
Provider teardown rejects new `run()` boundaries, removes the service so injected dependents drain, waits for returned Promise boundaries, then disables its `AsyncLocalStorage`. In-flight code retaining the service can call `current()` and `require()` while it drains; after disposal, all three methods throw `agent execution service is disposed`.
Async resources created inside `run()` inherit its Agent even when the operation does not await them. Agent-owned foreground work may inherit the boundary but keeps using the explicit cancellation and disposal contract of its execution seam. Unrelated timers, queues, and deployment infrastructure start under `run(undefined, operation)` and own an explicit stop. Queue, worker, process, and wire boundaries serialize any identity they need instead of relying on ALS propagation.
## Known Limitations and Deferred Work
- **Process-local only** — ALS does not cross workers, child processes, HTTP, durable queues, or restarts; each boundary materializes a typed identity explicitly.
- **Agent identity only** — turn, step, signal, cwd, sandbox, and authorization stay outside the frame until a concrete cross-cutting consumer justifies a separate design.
- **Ambient references may outlive liveness** — consumers still check `agent.status`, their explicit signal, and the owning capability contract before lifecycle-sensitive work.

View File

@@ -0,0 +1,31 @@
{
"name": "@deepseek-ai/dsh-agent-execution",
"description": "Agent-scoped asynchronous execution context for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,139 @@
/**
* Process-local Agent execution context backed by Node AsyncLocalStorage.
*
* @module @deepseek-ai/dsh-agent-execution
*/
import type { Context } from 'cordis'
import { AsyncLocalStorage } from 'node:async_hooks'
import type { AgentExecution } from './types.ts'
export type { AgentExecution } from './types.ts'
const NO_ACTIVE_EXECUTION = 'no agent execution context is active'
const DISPOSED_SERVICE = 'agent execution service is disposed'
/** Ambient Agent identity within one process-local asynchronous chain. */
export interface AgentExecutionService {
/**
* Read the active execution without requiring one.
* @returns the inherited execution, or `undefined` outside/inside a cleared boundary.
* @throws when this service instance has been disposed.
*/
current(): AgentExecution | undefined
/**
* Read the active execution and fail when no boundary is active.
* @returns the inherited execution.
* @throws when no execution is active or this service instance has been disposed.
*/
require(): AgentExecution
/**
* Run an operation inside an execution boundary. Passing `undefined` clears
* an inherited execution; the exact synchronous value or Promise is returned.
* @param execution - execution to inherit, or `undefined` for a clearing boundary.
* @param operation - synchronous or asynchronous operation to invoke.
* @returns the exact value returned by `operation`.
* @throws when this service is closing/disposed, or when `operation` throws.
*/
run<T>(execution: AgentExecution | undefined, operation: () => T): T
}
declare module 'cordis' {
interface Context {
agentExecution: AgentExecutionService
}
}
/** One provider-owned ALS instance with quiescent shutdown. */
class DefaultAgentExecutionService implements AgentExecutionService {
private readonly storage = new AsyncLocalStorage<AgentExecution | undefined>()
private state: 'active' | 'closing' | 'disposed' = 'active'
private activeRuns = 0
private drainWaiter: PromiseWithResolvers<void> | undefined
private disposalTask: Promise<void> | undefined
current(): AgentExecution | undefined {
this.assertReadable()
return this.storage.getStore()
}
require(): AgentExecution {
const execution = this.current()
if (execution === undefined) throw new Error(NO_ACTIVE_EXECUTION)
return execution
}
run<T>(execution: AgentExecution | undefined, operation: () => T): T {
if (this.state !== 'active') throw new Error(DISPOSED_SERVICE)
this.activeRuns += 1
let result: T
try {
result = this.storage.run(execution, operation)
} catch (error: unknown) {
this.releaseRun()
throw error
}
if (result instanceof Promise) {
void result.then(
() => { this.releaseRun() },
() => { this.releaseRun() },
)
} else {
this.releaseRun()
}
return result
}
/** Reject new boundaries while existing continuations remain readable. */
close(): void {
if (this.state === 'active') this.state = 'closing'
}
/** Wait for every returned Promise boundary, then invalidate retained references. */
dispose(): Promise<void> {
return (this.disposalTask ??= (async () => {
this.close()
if (this.activeRuns !== 0) {
this.drainWaiter ??= Promise.withResolvers<void>()
await this.drainWaiter.promise
}
this.state = 'disposed'
this.storage.disable()
})())
}
private assertReadable(): void {
if (this.state === 'disposed') throw new Error(DISPOSED_SERVICE)
}
private releaseRun(): void {
this.activeRuns -= 1
if (this.activeRuns !== 0) return
this.drainWaiter?.resolve()
this.drainWaiter = undefined
}
}
/** Cordis provider for the mandatory `ctx.agentExecution` service. */
export class AgentExecutionProvider {
private readonly service = new DefaultAgentExecutionService()
/**
* Install one isolated execution service and its ordered lifecycle.
* @param ctx - provider-owning Cordis context.
*/
constructor(ctx: Context) {
const service = this.service
ctx.effect(function* () {
// First yielded, disposed last: invalidate ALS only after dependents and active runs drain.
yield () => service.dispose()
yield ctx.provide('agentExecution', service)
// Last yielded, disposed first: prevent a teardown race from opening another boundary.
yield () => { service.close() }
}, 'agentExecution.lifecycle()')
}
}
export default AgentExecutionProvider

View File

@@ -0,0 +1,12 @@
/**
* Public Agent execution-context types.
*
* @module @deepseek-ai/dsh-agent-execution/types
*/
import type { Agent } from '@deepseek-ai/dsh-agent'
/** The exact live Agent associated with one asynchronous execution chain. */
export interface AgentExecution {
readonly agent: Agent
}

View File

@@ -0,0 +1,136 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import type { AgentExecution, AgentExecutionService } from '@deepseek-ai/dsh-agent-execution'
function execution(id: string): AgentExecution {
return { agent: { id: AgentId(id) } as Agent }
}
async function harness(): Promise<{
ctx: Context
service: AgentExecutionService
dispose: () => Promise<void>
}> {
const ctx = new Context()
const fiber = await ctx.plugin(AgentExecutionProvider)
return {
ctx,
service: ctx.agentExecution,
dispose: fiber.dispose,
}
}
describe('AgentExecutionProvider', () => {
it('reports an absent boundary and requires an active execution', async () => {
const { service, dispose } = await harness()
expect(service.current()).toBeUndefined()
expect(() => service.require()).toThrow('no agent execution context is active')
await dispose()
})
it('preserves exact synchronous and Promise return identities across await', async () => {
const { service, dispose } = await harness()
const active = execution('identity')
const value = { result: true }
expect(service.run(active, () => {
expect(service.require()).toBe(active)
return value
})).toBe(value)
const promise = service.run(active, async () => {
expect(service.require()).toBe(active)
await Promise.resolve()
expect(service.require()).toBe(active)
return value
})
expect(service.run(active, () => promise)).toBe(promise)
await expect(promise).resolves.toBe(value)
expect(service.current()).toBeUndefined()
await dispose()
})
it('isolates overlapping executions', async () => {
const { service, dispose } = await harness()
const a = execution('a')
const b = execution('b')
const bothStarted = Promise.withResolvers<boolean>()
const release = Promise.withResolvers<boolean>()
let starts = 0
const run = (active: AgentExecution): Promise<void> => service.run(active, async () => {
expect(service.require()).toBe(active)
starts += 1
if (starts === 2) bothStarted.resolve(true)
await release.promise
expect(service.require()).toBe(active)
})
const pending = [run(a), run(b)]
await bothStarted.promise
expect(service.current()).toBeUndefined()
release.resolve(true)
await Promise.all(pending)
await dispose()
})
it('restores nested and explicitly cleared boundaries', async () => {
const { service, dispose } = await harness()
const parent = execution('parent')
const child = execution('child')
service.run(parent, () => {
expect(service.require()).toBe(parent)
service.run(child, () => { expect(service.require()).toBe(child) })
expect(service.require()).toBe(parent)
service.run(undefined, () => {
expect(service.current()).toBeUndefined()
expect(() => service.require()).toThrow('no agent execution context is active')
})
expect(service.require()).toBe(parent)
})
expect(service.current()).toBeUndefined()
await dispose()
})
it('restores context after synchronous throws and rejected operations', async () => {
const { service, dispose } = await harness()
const parent = execution('parent')
const child = execution('child')
const syncError = new Error('sync failure')
const asyncError = new Error('async failure')
service.run(parent, () => {
expect(() => service.run(child, () => { throw syncError })).toThrow(syncError)
expect(service.require()).toBe(parent)
})
await expect(service.run(child, async () => {
await Promise.resolve()
throw asyncError
})).rejects.toBe(asyncError)
expect(service.current()).toBeUndefined()
await dispose()
})
it('stops new boundaries, drains active Promises, and invalidates retained references', async () => {
const { ctx, service, dispose } = await harness()
const active = execution('draining')
const release = Promise.withResolvers<boolean>()
const pending = service.run(active, async () => {
await release.promise
expect(service.require()).toBe(active)
})
let disposed = false
const disposal = dispose().then(() => { disposed = true })
await Promise.resolve()
expect(() => service.run(active, () => 1)).toThrow('agent execution service is disposed')
expect(disposed).toBe(false)
expect(ctx.get('agentExecution')).toBeUndefined()
release.resolve(true)
await pending
await disposal
expect(() => service.current()).toThrow('agent execution service is disposed')
expect(() => service.require()).toThrow('agent execution service is disposed')
})
})

View File

@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../core/agent"
}
]
}

View File

@@ -23,7 +23,7 @@ The config-driven `ctx.agentLoop.create()` path keeps its agent owned by the loo
### Injected services
`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface services.
`agents`, `agentExecution`, `sessions`, `llm`, `tools`, `systemPrompt` — all six interface services. The loop cannot activate without `agentExecution`; the default bundle loads its provider before the loop.
### Configuration (schemastery)
@@ -48,7 +48,9 @@ Configured agents start automatically. `cwd` applies only to fresh sessions; `re
### Loop lifecycle (`loop.ts`)
The driver owns one agent for its lifetime. It records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures.
The driver owns one agent for its lifetime and runs inside `ctx.agentExecution.run({ agent }, ...)`, so process-local asynchronous continuations can recover the initiating Agent. Creation, persistence load, and unpublished setup stay outside the child boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [execution-context package](../agent-execution/README.md) owns propagation and detached-work rules.
The loop records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures.
Plugin failure ends the current turn, not the loop. Cancellation clears pending work and aborts the current step without leaking to the next prompt. Terminal continuation stops remain authoritative through turn close and durability flush.

View File

@@ -22,6 +22,7 @@
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-execution": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -35,6 +36,7 @@
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-execution": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",

View File

@@ -327,7 +327,7 @@ export class ReactLoopAgent implements Agent {
[startDriver](): void {
if (this._status === 'disposed') return
this.driverStarted = true
this.done = runLoop(this.loopCtx, this, {
this.done = this.loopCtx.agentExecution.run({ agent: this }, () => runLoop(this.loopCtx, this, {
inbox: this.#inbox,
setStatus: (status) => { this.setStatus(status) },
setAbort: controller => void (this.currentAbort = controller),
@@ -338,7 +338,7 @@ export class ReactLoopAgent implements Agent {
clearCancel: () => { this.cancelRequested = false },
// Pre-step cancellation re-parks without emitting a status transition.
settleIdle: () => { this.settleIdleWaiters() },
})
}))
}
/**

View File

@@ -11,6 +11,7 @@ import z from 'schemastery'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-execution'
import type {
AgentFactory,
AgentHandle,
@@ -333,7 +334,7 @@ export interface Config {
/** Concrete ReactLoopAgent factory and driver service. */
export class AgentLoop extends Service implements AgentFactory {
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
static inject = ['agents', 'agentExecution', 'sessions', 'llm', 'tools', 'systemPrompt']
/** Runtime schema for declarative agents. */
static Config = z.object({

View File

@@ -0,0 +1,374 @@
import { describe, expect, it } from 'vitest'
import { Context, FiberState, type Fiber } from 'cordis'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import type { AgentExecutionService } from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
interface Harness {
ctx: Context
providerFiber: Fiber
loopFiber: Fiber
}
async function harness(adapter: LlmAdapter): Promise<Harness> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const providerFiber = await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, providerFiber, loopFiber }
}
function waitForIdle(ctx: Context, agent: ReactLoopAgent | Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject === agent && status === 'idle') {
dispose()
resolve()
}
})
})
}
function send(agent: Agent, text: string): void {
agent.send([{ type: 'text', text }])
}
/** Adapter that holds both drivers at the same awaited continuation. */
class OverlapAdapter extends LlmAdapter {
private readonly bothStarted = Promise.withResolvers<boolean>()
private starts = 0
readonly observations: { sessionId: SessionId | undefined; before: Agent; after: Agent }[] = []
constructor(private readonly ctx: Context) {
super()
}
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const before = this.ctx.agentExecution.require().agent
this.starts += 1
if (this.starts === 2) this.bothStarted.resolve(true)
await this.bothStarted.promise
await Promise.resolve()
const after = this.ctx.agentExecution.require().agent
this.observations.push({ sessionId: options.sessionId, before, after })
yield* textResponse('done')
}
}
/** Test-only transport that materializes ambient identity at its request boundary. */
class TestCapabilityTransport {
readonly requests: { path: string; headers: Record<string, string> }[] = []
constructor(private readonly execution: AgentExecutionService) {}
async request(path: string): Promise<Record<string, string>> {
await Promise.resolve()
const headers = {
'X-Harness-Session-Id': this.execution.require().agent.session.id,
}
this.requests.push({ path, headers })
return headers
}
}
/** Adapter whose first call waits for cancellation and whose later calls complete. */
class ReloadAdapter extends LlmAdapter {
readonly firstStarted = Promise.withResolvers<boolean>()
firstAgentDuringAbort: Agent | undefined
laterAgent: Agent | undefined
calls = 0
execution: AgentExecutionService | undefined
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const execution = this.execution
if (execution === undefined) throw new Error('execution service missing')
this.calls += 1
if (this.calls === 1) {
this.firstStarted.resolve(true)
try {
await new Promise<void>((_resolve, reject) => {
const abort = (): void => { reject(new Error('aborted')) }
if (options.signal?.aborted === true) abort()
else options.signal?.addEventListener('abort', abort, { once: true })
})
} catch (error: unknown) {
await Promise.resolve()
this.firstAgentDuringAbort = execution.require().agent
throw error
}
return
}
await Promise.resolve()
this.laterAgent = execution.require().agent
yield* textResponse('reloaded')
}
}
describe('AgentLoop execution context', () => {
it('keeps overlapping driver continuations bound to their exact Agents', async () => {
const ctx = new Context()
const adapter = new OverlapAdapter(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
const a = ctx.agentLoop.create(AgentId('a'), { model: 'mock' })
const b = ctx.agentLoop.create(AgentId('b'), { model: 'mock' })
const idleA = waitForIdle(ctx, a)
const idleB = waitForIdle(ctx, b)
send(a, 'a')
send(b, 'b')
await Promise.all([idleA, idleB])
expect(adapter.observations).toHaveLength(2)
expect(adapter.observations).toEqual(expect.arrayContaining([
{ sessionId: a.session.id, before: a, after: a },
{ sessionId: b.session.id, before: b, after: b },
]))
expect(ctx.agentExecution.current()).toBeUndefined()
await ctx.fiber.dispose()
})
it('keeps child setup under the parent boundary, switches for the child driver, then restores the parent', async () => {
const adapter = new MockAdapter([
toolCallResponse('spawn', 'spawn-child', {}),
toolCallResponse('observe', 'observe-child', {}),
textResponse('child done'),
textResponse('parent done'),
])
const { ctx } = await harness(adapter)
let parentDuringSetup: Agent | undefined
let explicitChild: Agent | undefined
let childDuringDriver: Agent | undefined
let parentAfterChild: Agent | undefined
let child: Agent | undefined
ctx.tools.register(defineTool({
name: 'spawn-child',
description: 'create one child agent',
parameters: {},
execute: async (_args, exec) => {
if (exec.agent === undefined) throw new Error('parent agent missing')
const handle = await exec.agent.ctx.agents.create({
agentId: AgentId('child'),
sessionId: SessionId('child-session'),
agentOptions: { model: 'mock' },
setup: (agentCtx) => {
parentDuringSetup = ctx.agentExecution.require().agent
explicitChild = agentCtx.agent
agentCtx.tools.register(defineTool({
name: 'observe-child',
description: 'observe child execution identity',
parameters: {},
execute: async () => {
await Promise.resolve()
childDuringDriver = ctx.agentExecution.require().agent
return [{ type: 'text', text: 'observed' }]
},
}))
},
})
child = handle.agent
send(handle.agent, 'run child')
await handle.agent.whenIdle()
parentAfterChild = ctx.agentExecution.require().agent
await handle.dispose()
return [{ type: 'text', text: 'child completed' }]
},
}))
const parentHandle = await ctx.agents.create({
agentId: AgentId('parent'),
sessionId: SessionId('parent-session'),
agentOptions: { model: 'mock' },
})
const idle = waitForIdle(ctx, parentHandle.agent)
send(parentHandle.agent, 'spawn')
await idle
expect(parentDuringSetup).toBe(parentHandle.agent)
expect(explicitChild).toBe(child)
expect(childDuringDriver).toBe(child)
expect(parentAfterChild).toBe(parentHandle.agent)
expect(ctx.agentExecution.current()).toBeUndefined()
await parentHandle.dispose()
await ctx.fiber.dispose()
})
it('keeps agentless direct tools ambient-free and builds trusted transport headers internally', async () => {
const adapter = new MockAdapter([
toolCallResponse('capability', 'capability-request', { path: '/v1/capability' }),
textResponse('done'),
])
const { ctx } = await harness(adapter)
const transport = new TestCapabilityTransport(ctx.agentExecution)
let directAmbient: Agent | undefined
let captured: Agent | undefined
ctx.tools.register(defineTool({
name: 'agentless-probe',
description: 'observe an agentless call',
parameters: {},
execute: async () => {
await Promise.resolve()
directAmbient = ctx.agentExecution.current()?.agent
return [{ type: 'text', text: 'ok' }]
},
}))
ctx.tools.register(defineTool({
name: 'capability-request',
description: 'call the test capability transport',
parameters: { path: { type: 'string' } },
execute: async (args) => {
captured = ctx.agentExecution.require().agent
const path = (args as { path: string }).path
const headers = await transport.request(path)
return [{ type: 'text', text: JSON.stringify(headers) }]
},
}))
const direct = await ctx.tools.execute({
callId: CallId('direct'),
name: 'agentless-probe',
arguments: {},
})
expect(direct.isError).toBe(false)
expect(directAmbient).toBeUndefined()
const handle = await ctx.agents.create({
agentId: AgentId('transport'),
sessionId: SessionId('transport-session'),
agentOptions: { model: 'mock' },
})
const idle = waitForIdle(ctx, handle.agent)
send(handle.agent, 'call transport')
await idle
expect(transport.requests).toEqual([{
path: '/v1/capability',
headers: { 'X-Harness-Session-Id': 'transport-session' },
}])
const schema = adapter.requests[0]?.tools?.find(tool => tool.name === 'capability-request')
expect(JSON.stringify(schema?.parameters)).not.toMatch(/session|harness/i)
const call = handle.agent.session.events.find(event => event.type === 'tool/call')
expect(call?.type === 'tool/call' ? call.data.arguments : undefined)
.toBe(JSON.stringify({ path: '/v1/capability' }))
expect(captured).toBe(handle.agent)
await handle.dispose()
expect(captured?.status).toBe('disposed')
expect(ctx.agentExecution.current()).toBeUndefined()
await ctx.fiber.dispose()
})
it('keeps AgentLoop inactive until the mandatory provider appears', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const loopFiber = ctx.plugin(AgentLoop, { agents: [] })
await Promise.resolve()
expect(loopFiber.state).toBe(FiberState.PENDING)
await ctx.plugin(AgentExecutionProvider)
await loopFiber
expect(loopFiber.state).toBe(FiberState.ACTIVE)
await ctx.fiber.dispose()
})
it('drains the old driver before disabling ALS during provider restart', async () => {
const ctx = new Context()
const adapter = new ReloadAdapter()
const { providerFiber, loopFiber } = await (async (): Promise<Harness> => {
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const mountedProvider = await ctx.plugin(AgentExecutionProvider)
const mountedLoop = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, providerFiber: mountedProvider, loopFiber: mountedLoop }
})()
const oldService = ctx.agentExecution
adapter.execution = oldService
const oldHandle = await ctx.agents.create({
agentId: AgentId('before-restart'),
sessionId: SessionId('before-restart-session'),
agentOptions: { model: 'mock' },
})
const oldAgent = oldHandle.agent
send(oldAgent, 'block')
await adapter.firstStarted.promise
await providerFiber.restart()
await loopFiber.await()
expect(adapter.firstAgentDuringAbort?.id).toBe(oldAgent.id)
expect(adapter.firstAgentDuringAbort?.session).toBe(oldAgent.session)
expect(oldAgent.status).toBe('disposed')
expect(() => oldService.current()).toThrow('agent execution service is disposed')
expect(ctx.agentExecution).not.toBe(oldService)
adapter.execution = ctx.agentExecution
const newHandle = await ctx.agents.create({
agentId: AgentId('after-restart'),
sessionId: SessionId('after-restart-session'),
agentOptions: { model: 'mock' },
})
const newAgent = newHandle.agent
const idle = waitForIdle(ctx, newAgent)
send(newAgent, 'continue')
await idle
expect(adapter.laterAgent?.id).toBe(newAgent.id)
expect(adapter.laterAgent?.session).toBe(newAgent.session)
await ctx.fiber.dispose()
})
it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => {
const ctx = new Context()
const adapter = new ReloadAdapter()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
const service = ctx.agentExecution
adapter.execution = service
const handle = await ctx.agents.create({
agentId: AgentId('root-dispose'),
sessionId: SessionId('root-dispose-session'),
agentOptions: { model: 'mock' },
})
const agent = handle.agent
send(agent, 'block')
await adapter.firstStarted.promise
await ctx.fiber.dispose()
expect(adapter.firstAgentDuringAbort?.id).toBe(agent.id)
expect(adapter.firstAgentDuringAbort?.session).toBe(agent.session)
expect(agent.status).toBe('disposed')
expect(() => service.current()).toThrow('agent execution service is disposed')
})
})

View File

@@ -6,6 +6,7 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { bindReactLoopAgentContext, prepareReactLoopAgent } from '../src/agent.ts'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -17,6 +18,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -51,6 +53,7 @@ function send(agent: ReactLoopAgent, text: string) {
describe('ReactLoopAgent', () => {
it('rejects access before context binding and a second driver for one session', async () => {
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('exclusive-driver'))
const prepared = prepareReactLoopAgent(ctx, AgentId('first-driver'), { model: 'mock' }, session)
@@ -252,6 +255,7 @@ describe('ReactLoopAgent', () => {
it('disposer is idempotent (double-stop)', async () => {
// The internal start seam exposes one idle driver's disposer for repeated invocation.
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('test'))
const prepared = prepareReactLoopAgent(ctx, AgentId('bare'), { model: 'mock' }, session)
@@ -361,6 +365,7 @@ describe('ReactLoopAgent', () => {
// Queue the internal waiter while running, then dispose the bare driver. Its disposed branch
// must chain the loop's `done` promise rather than resolve before exit.
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)

View File

@@ -14,6 +14,7 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -194,6 +196,7 @@ describe('Agent.cancel()', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -319,6 +322,7 @@ describe('Agent.cancel()', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -9,6 +9,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -31,6 +32,7 @@ describe('config-driven session id', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('deferred') }],
})
@@ -53,6 +55,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentExecutionProvider)
await ctx1.plugin(AgentLoop, { agents: [{ id: AgentId('cfg'), model: 'mock' }] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg')]))
@@ -70,6 +73,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [{ id: AgentId('cfg'), model: 'mock' }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg2')]))
@@ -93,6 +97,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentExecutionProvider)
await ctx1.plugin(AgentLoop, { agents: [] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')]))
@@ -109,6 +114,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('sticky-1') }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')]))
@@ -137,6 +143,7 @@ describe('config-driven session id', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('does-not-exist') }] })
const warn = vi.spyOn((ctx.agentLoop as unknown as { ctx: { logger: { warn: (...a: unknown[]) => void } } }).ctx.logger, 'warn')
.mockImplementation(() => undefined)

View File

@@ -5,6 +5,7 @@ import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason } from '@
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type ContinuationDecision } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { prepareReactLoopAgent } from '../src/agent.ts'
import * as Invariants from '@deepseek-ai/dsh-invariants'
@@ -19,6 +20,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -524,6 +526,7 @@ describe('turn numbering continues across seeded sessions', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
ctx2.llm.registerAdapter(['mock'], second)
@@ -664,6 +667,7 @@ describe('turn and step boundary recovery', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1114,6 +1118,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1165,6 +1170,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1220,6 +1226,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1271,6 +1278,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1320,6 +1328,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -6,6 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -16,6 +17,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -10,6 +10,7 @@ import AgentRegistry, {
type PromptDecision,
type SessionStartSource,
} from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -29,6 +30,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -15,6 +16,7 @@ async function harness(adapter: MockAdapter, persona = '') {
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -924,6 +926,7 @@ describe('agent loop', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('config-agent'), model: 'mock' }],
})
@@ -947,6 +950,7 @@ describe('agent loop', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('config-agent'), model: 'mock', cwd: '/work/project' }],
})

View File

@@ -13,6 +13,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import fc from 'fast-check'
@@ -36,6 +37,7 @@ async function harness() {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], new EchoAdapter())
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
@@ -42,6 +43,7 @@ async function loopHarness(): Promise<Context> {
await created.plugin(SystemPrompt, { persona: SYSTEM })
await created.plugin(ToolRegistry)
await created.plugin(AgentRegistry)
await created.plugin(AgentExecutionProvider)
await created.plugin(AgentLoop, { agents: [] })
await created.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
created.tools.register(defineTool({

View File

@@ -14,6 +14,7 @@ import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-a
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter, persona = 'stable base') {
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -10,6 +10,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -29,6 +30,7 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -138,6 +140,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -166,6 +169,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -388,6 +392,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('next')]))
@@ -449,6 +454,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -502,6 +508,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -531,6 +538,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -561,6 +569,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
await expect(ctx.agents.resume({ agentId: AgentId('m'), resumeSessionId: SessionId('nope') }))

View File

@@ -7,6 +7,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -18,6 +19,7 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, loopFiber }

View File

@@ -14,6 +14,7 @@ import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore, { type TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type ContinuationStop } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -17,6 +18,7 @@ async function harness(adapter: MockAdapter): Promise<Context> {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(Invariants)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -35,6 +35,9 @@
{
"path": "../../core/agent"
},
{
"path": "../../core/agent-execution"
},
{
"path": "../../core/scope"
}

View File

@@ -160,6 +160,26 @@ export class FixService {
expect(services[0]?.methods).toHaveLength(3)
})
it('extracts an interface service as an abstract seam', () => {
const services = collectServices(makeService(`/** Fixture service interface. */
export interface FixService {
/**
* Do the thing.
* @param id - which thing to do.
* @returns the outcome of doing it.
*/
run(id: string): string
}`))
expect(services).toHaveLength(1)
expect(services[0]).toMatchObject({
key: 'fix',
type: 'FixService',
abstract: true,
doc: 'Fixture service interface.',
})
expect(services[0]?.methods).toEqual(['run(id: string): string'])
})
it('hard-errors on a public method with no JSDoc at all', () => {
expect(() => collectServices(makeService(
'/** Fixture service. */\nexport class FixService {\n run(id: string): string { return id }\n}',