Explorar el Código

Merge branch 'codex/simp-unify-agent-session-id' into codex/simp-ui-identity-residue

Tianyi Cui hace 2 meses
padre
commit
0decadd5e4

+ 1 - 1
docs/config-catalog.md

@@ -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`
 ## `@deepseek-ai/dsh-sandbox-local`
 
 

+ 1 - 1
docs/cordis-catalog/services.md

@@ -19,7 +19,7 @@ async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<Agent
 async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
 async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
 ```
 ```
 
 
-Source: [`packages/core/agent-loop/src/index.ts:373`](../../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`
 ## `ctx.agents` — `AgentRegistry`
 
 

+ 19 - 3
packages/core/agent-loop/src/index.ts

@@ -369,6 +369,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 ReactLoopAgent factory and driver service. */
 /** Concrete ReactLoopAgent factory and driver service. */
 export class AgentLoop extends Service implements AgentFactory {
 export class AgentLoop extends Service implements AgentFactory {
   static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
   static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
@@ -390,6 +408,7 @@ export class AgentLoop extends Service implements AgentFactory {
 
 
   constructor(ctx: Context, public config: Config) {
   constructor(ctx: Context, public config: Config) {
     super(ctx, 'agentLoop')
     super(ctx, 'agentLoop')
+    validateConfiguredAgents(config.agents)
     this.ownership = new FactoryOwnership(ctx.fiber)
     this.ownership = new FactoryOwnership(ctx.fiber)
     this.runtime = { ctx }
     this.runtime = { ctx }
     ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()')
     ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()')
@@ -412,9 +431,6 @@ export class AgentLoop extends Service implements AgentFactory {
         }
         }
         continue
         continue
       }
       }
-      if (sessionId !== undefined) {
-        throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`)
-      }
       ctx.effect(() => {
       ctx.effect(() => {
         const fiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => {
         const fiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => {
           void this.resumeWith(ctx, childCtx.sessionPersistence, {
           void this.resumeWith(ctx, childCtx.sessionPersistence, {

+ 19 - 0
packages/core/agent-loop/tests/config-session-id.spec.ts

@@ -64,6 +64,25 @@ describe('config-driven session id', () => {
     await conflicting.fiber.dispose()
     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 () => {
   it('restores a materialized exact id across an AgentLoop-only reload', async () => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-reload-'))
     const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-reload-'))
     dirs.push(root)
     dirs.push(root)

+ 4 - 33
packages/guard/repeat-tool-guard/src/index.ts

@@ -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
  * @module @deepseek-ai/dsh-repeat-tool-guard
  */
  */