Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id
# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/module-graph.md # examples/coding-agent/tests/code-mode.e2e.ts # examples/coding-agent/tests/coding-task.e2e.ts # examples/coding-agent/tests/compaction.e2e.ts # examples/coding-agent/tests/full-loop.e2e.ts # examples/coding-agent/tests/todo-write.e2e.ts # examples/cordis-agent/tests/cordis-tools.e2e.ts # packages/bash/tool-bash/tests/integration.spec.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/compact/compact-basic/tests/compact-loop-repro.spec.ts # packages/context/time-context/tests/time-context.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/cordis/tool-cordis/tests/integration.spec.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/agent.ts # packages/core/agent-loop/src/index.ts # packages/core/agent-loop/tests/agent.spec.ts # packages/core/agent-loop/tests/cancel.spec.ts # packages/core/agent-loop/tests/config-session-id.spec.ts # packages/core/agent-loop/tests/contract-regressions.spec.ts # packages/core/agent-loop/tests/coverage-edges.spec.ts # packages/core/agent-loop/tests/interception.spec.ts # packages/core/agent-loop/tests/loop.spec.ts # packages/core/agent-loop/tests/properties.spec.ts # packages/core/agent-loop/tests/request-cache.e2e.ts # packages/core/agent-loop/tests/request-reconstruction.spec.ts # packages/core/agent-loop/tests/resume.spec.ts # packages/core/agent-loop/tests/scope-lifecycle.spec.ts # packages/core/agent-loop/tests/tool-order.spec.ts # packages/core/agent-loop/tests/turn-stop.spec.ts # packages/core/agent/src/types.ts # packages/examples/agent-spine-demo/README.md # packages/examples/agent-spine-demo/tests/agent-core.spec.ts # packages/examples/stdio-demo/README.md # packages/examples/stdio-demo/src/index.ts # packages/examples/stdio-demo/tests/stdio-agent.spec.ts # packages/fs/tool-fs/tests/fs-tools.e2e.ts # packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts # packages/hooks/hooks-claude/tests/bridge.spec.ts # packages/hooks/hooks-claude/tests/coverage.spec.ts # packages/hooks/hooks-codex/tests/bridge.spec.ts # packages/hooks/hooks-codex/tests/coverage.spec.ts # packages/subagent/subagent-fork/tests/multi-subagent.spec.ts # packages/subagent/subagent-fork/tests/subagent-fork.spec.ts # packages/subagent/subagent-inprocess/tests/structured.spec.ts # packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts # packages/subagent/subagent-spawn/tests/spawn.e2e.ts # packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts # packages/todo/tool-todo/tests/integration.spec.ts # packages/ui/acp/tests/dispose.spec.ts # packages/ui/acp/tests/edges.spec.ts # packages/workflow/workflow-workerthread/tests/integration.spec.ts
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, 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 from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import * as RepeatToolGuard from '@deepseek-ai/dsh-repeat-tool-guard'
|
||||
import type { Config } from '@deepseek-ai/dsh-repeat-tool-guard'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
@@ -22,11 +20,7 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
|
||||
/** Boot the core spine + the guard; the caller registers adapters and extra listeners. */
|
||||
async function harness(config: Config = {}): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(RepeatToolGuard, config)
|
||||
ctx.tools.register(defineTool({ name: 'probe', description: 'p', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } }))
|
||||
@@ -58,7 +52,7 @@ describe('threshold escalation', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -79,7 +73,7 @@ describe('threshold escalation', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -101,7 +95,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -125,7 +119,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -143,7 +137,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -164,7 +158,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -180,7 +174,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -196,7 +190,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -216,8 +210,8 @@ describe('chain semantics', () => {
|
||||
toolCallResponse('b3', 'probe', { q: 1 }),
|
||||
textResponse('done'),
|
||||
]))
|
||||
const agentA = ctx.agentLoop.create(SessionId('a'), { model: 'mock-a' })
|
||||
const agentB = ctx.agentLoop.create(SessionId('b'), { model: 'mock-b' })
|
||||
const agentA = ctx.agentLoop.create(SessionId('a'), { provider: 'mock-a', model: 'model-a' })
|
||||
const agentB = ctx.agentLoop.create(SessionId('b'), { provider: 'mock-b', model: 'model-b' })
|
||||
agentA.send([{ type: 'text', text: 'go' }])
|
||||
agentB.send([{ type: 'text', text: 'go' }])
|
||||
await Promise.all([waitForIdle(ctx, agentA), waitForIdle(ctx, agentB)])
|
||||
@@ -236,7 +230,7 @@ describe('chain semantics', () => {
|
||||
textResponse('turn two done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
agent.send([{ type: 'text', text: 'again' }])
|
||||
@@ -257,14 +251,14 @@ describe('chain semantics', () => {
|
||||
// (the loop.spec pattern): a child plugin fiber owns `first`.
|
||||
let first!: ReactLoopAgent
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
first = inner.agentLoop.create(SessionId('reused'), { model: 'mock' })
|
||||
first = inner.agentLoop.create(SessionId('reused'), { provider: 'mock', model: 'mock' })
|
||||
}, { inject: ['agentLoop'] }))
|
||||
first.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, first)
|
||||
await fiber.dispose()
|
||||
await first.done
|
||||
|
||||
const second = ctx.agentLoop.create(SessionId('reused'), { model: 'mock' })
|
||||
const second = ctx.agentLoop.create(SessionId('reused'), { provider: 'mock', model: 'mock' })
|
||||
second.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, second)
|
||||
|
||||
@@ -280,7 +274,7 @@ describe('chain semantics', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -296,7 +290,7 @@ describe('chain semantics', () => {
|
||||
toolCallResponse('c1', 'probe', { q: 1 }), // if the direct call had counted, this would be #2
|
||||
textResponse('done'),
|
||||
]))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -310,7 +304,7 @@ describe('fold onto the downstream decision', () => {
|
||||
ctx.on('tools/post-execute', async () => ({
|
||||
kind: 'block' as const,
|
||||
feedback: [{ type: 'text' as const, text: 'nope' }],
|
||||
additionalContext: { content: [{ type: 'text' as const, text: 'downstream-ctx' }], source: { kind: 'plugin' as const, plugin: 'test' } },
|
||||
additionalContexts: [{ content: [{ type: 'text' as const, text: 'downstream-ctx' }], source: { kind: 'plugin' as const, plugin: 'test' } }],
|
||||
}))
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('c1', 'probe', { q: 1 }),
|
||||
@@ -318,19 +312,19 @@ describe('fold onto the downstream decision', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const found = reminders(agent)
|
||||
expect(found).toHaveLength(2)
|
||||
expect(found).toHaveLength(3)
|
||||
// Call 1: below threshold — the downstream context passes through untouched.
|
||||
expect(found[0]!.text).toBe('downstream-ctx')
|
||||
expect(found[0]!.source).toEqual({ kind: 'plugin', plugin: 'test' })
|
||||
// Call 2: reminder folded in front, single merged context, the guard's source.
|
||||
// Call 2: reminder and downstream context retain separate provenance.
|
||||
expect(found[1]!.text).toContain('repeating the exact same tool call')
|
||||
expect(found[1]!.text).toContain('|downstream-ctx')
|
||||
expect(found[1]!.source).toEqual(GUARD_SOURCE)
|
||||
expect(found[2]).toEqual({ text: 'downstream-ctx', source: { kind: 'plugin', plugin: 'test' } })
|
||||
// The block's feedback reached the tool result unchanged.
|
||||
const results = [...agent.session.events].filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
expect(results.every(r => r.data.isError)).toBe(true)
|
||||
@@ -349,7 +343,7 @@ describe('fold onto the downstream decision', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
@@ -364,11 +358,7 @@ describe('fold onto the downstream decision', () => {
|
||||
describe('config validation fails loud', () => {
|
||||
async function spine(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
return ctx
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user