mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'codex/simp-ui-identity-residue' into codex/simp-hide-concrete-agent-loop
# Conflicts: # docs/cordis-catalog/services.md # packages/core/agent-loop/src/index.ts
This commit is contained in:
@@ -554,7 +554,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/guard/repeat-tool-guard/src/index.ts:55`](../packages/guard/repeat-tool-guard/src/index.ts)
|
||||
Source: [`packages/guard/repeat-tool-guard/src/index.ts:26`](../packages/guard/repeat-tool-guard/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-sandbox-local`
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<Agent
|
||||
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent-loop/src/index.ts:372`](../../packages/core/agent-loop/src/index.ts)
|
||||
Source: [`packages/core/agent-loop/src/index.ts:391`](../../packages/core/agent-loop/src/index.ts)
|
||||
|
||||
## `ctx.agents` — `AgentRegistry`
|
||||
|
||||
|
||||
@@ -368,6 +368,24 @@ export interface Config {
|
||||
})[]
|
||||
}
|
||||
|
||||
/** Reject self-contained identity conflicts before any configured agent starts. */
|
||||
function validateConfiguredAgents(agents: Config['agents']): void {
|
||||
const exactIdentities = new Map<SessionId, string>()
|
||||
for (const { id, sessionId, resumeSessionId } of agents) {
|
||||
const hasResumeId = resumeSessionId !== undefined && resumeSessionId !== ''
|
||||
if (sessionId !== undefined && hasResumeId) {
|
||||
throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`)
|
||||
}
|
||||
const exactIdentity = hasResumeId ? resumeSessionId : sessionId
|
||||
if (exactIdentity === undefined) continue
|
||||
const firstId = exactIdentities.get(exactIdentity)
|
||||
if (firstId !== undefined) {
|
||||
throw new Error(`agents "${firstId}" and "${id}" use duplicate exact session identity "${exactIdentity}"`)
|
||||
}
|
||||
exactIdentities.set(exactIdentity, id)
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete agent factory and driver service. */
|
||||
export class AgentLoop extends Service implements AgentFactory {
|
||||
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
|
||||
@@ -389,6 +407,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
|
||||
constructor(ctx: Context, public config: Config) {
|
||||
super(ctx, 'agentLoop')
|
||||
validateConfiguredAgents(config.agents)
|
||||
this.ownership = new FactoryOwnership(ctx.fiber)
|
||||
this.runtime = { ctx }
|
||||
ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()')
|
||||
@@ -411,9 +430,6 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (sessionId !== undefined) {
|
||||
throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`)
|
||||
}
|
||||
ctx.effect(() => {
|
||||
const fiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => {
|
||||
void this.resumeWith(ctx, childCtx.sessionPersistence, {
|
||||
|
||||
@@ -64,6 +64,25 @@ describe('config-driven session id', () => {
|
||||
await conflicting.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects duplicate exact ids before asynchronous configured startup', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-duplicate-'))
|
||||
dirs.push(root)
|
||||
const ctx = await makeCoreContext()
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root })
|
||||
|
||||
const outcome = await ctx.plugin(AgentLoop, {
|
||||
agents: [
|
||||
{ id: 'first', sessionId: SessionId('shared'), model: 'mock' },
|
||||
{ id: 'second', sessionId: SessionId('shared'), model: 'mock' },
|
||||
],
|
||||
}).then(() => undefined, (error: unknown) => error)
|
||||
const published = ctx.agents.get(SessionId('shared'))
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
expect(outcome).toEqual(new Error('agents "first" and "second" use duplicate exact session identity "shared"'))
|
||||
expect(published).toBeUndefined()
|
||||
})
|
||||
|
||||
it('restores a materialized exact id across an AgentLoop-only reload', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-reload-'))
|
||||
dirs.push(root)
|
||||
|
||||
@@ -1,37 +1,8 @@
|
||||
/**
|
||||
* Repeat-tool-call guard: advisory loop-breaker for agents stuck re-issuing
|
||||
* the same tool call with identical arguments.
|
||||
*
|
||||
* Not a model-facing tool — it registers no tool, never vetoes or rewrites a
|
||||
* call, and adds exactly one behavior: watch each agent's stream of tool calls
|
||||
* through the `tools/post-execute` waterfall, count runs of consecutive calls
|
||||
* to the same tool with identical canonicalized arguments, and at configured
|
||||
* run lengths fold an escalating advisory reminder onto the decision's
|
||||
* `additionalContext`. The loop appends that context as a logged
|
||||
* `context/message` after the step's tool results, so the reminder is
|
||||
* model-visible, source-attributed, and reconstructable from the session log
|
||||
* with no new session event. Decision record:
|
||||
* docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md.
|
||||
*
|
||||
* ```yaml
|
||||
* - id: repeat-tool-guard
|
||||
* name: '@deepseek-ai/dsh-repeat-tool-guard'
|
||||
* config:
|
||||
* thresholds: [3, 5, 8] # consecutive counts that trigger a reminder
|
||||
* include: [] # tool-name patterns to track; empty = all tools
|
||||
* exclude: [todo_write] # tool-name patterns transparent to the chain
|
||||
* ```
|
||||
*
|
||||
* Chain state is keyed by the live agent object — the tool registry is a
|
||||
* context-level singleton whose waterfalls interleave every agent's calls, so
|
||||
* a shared counter would let one agent's repetition trip another's reminder.
|
||||
* State is in-memory only: a session resumed from persistence starts with a
|
||||
* fresh chain (the guard is a heuristic nudge, not a logged invariant).
|
||||
*
|
||||
* Plugin export shape: named exports, NO default. The cordis Loader's
|
||||
* `unwrapExports` does `exports.default ?? exports`, so a stray default would
|
||||
* collapse the module to the bare `apply` (see docs/postmortem/0001).
|
||||
*
|
||||
* Advisory per-agent repeat-call detector. It enriches post-execute decisions
|
||||
* with logged model context without vetoing or rewriting calls. Configuration
|
||||
* and chain semantics live in the package README; rationale lives in the
|
||||
* repeat-tool-guard RFC.
|
||||
* @module @deepseek-ai/dsh-repeat-tool-guard
|
||||
*/
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@ export interface SubagentResult {
|
||||
export interface SubagentRun {
|
||||
/**
|
||||
* Parent-scoped run id. For a local run, this MUST equal the published child
|
||||
* session id, whose `parentSession` records `request.parent`; a remote
|
||||
* provider mints an id unique in the parent namespace.
|
||||
* session id, whose `parentSession` records `request.parent.session.id`; a
|
||||
* remote provider mints an id unique in the parent namespace.
|
||||
*/
|
||||
readonly id: SessionId
|
||||
/**
|
||||
|
||||
@@ -1049,7 +1049,7 @@ function validateMcpServers(params: { mcpServers?: unknown[] }): void {
|
||||
* zero or more times per event (best-effort UI feed, never load-bearing).
|
||||
* @param presenter - resolves tool-owned render intent for tool events;
|
||||
* defaults to the generic-fallback {@link nullToolPresenter}.
|
||||
* @param terminal - the connection's terminal-rendering context; defaults to
|
||||
* @param terminal - the session's terminal-rendering context; defaults to
|
||||
* disabled (the plain-text console-block fallback).
|
||||
* @param options - `includeUserMessages` (default `true`): live streaming
|
||||
* passes `false` so a prompt the client just sent is not echoed back.
|
||||
@@ -1122,7 +1122,7 @@ export function todosToPlan(todos: TodoItem[]): Plan {
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-connection terminal-rendering context threaded into
|
||||
* Per-session terminal-rendering context threaded into
|
||||
* {@link streamSessionEventUpdate}: whether the client advertised the
|
||||
* `_meta.terminal_output` capability, and the session's workspace cwd (the
|
||||
* default terminal-card header when a tool doesn't supply its own). Kept out of
|
||||
|
||||
Reference in New Issue
Block a user