Skip to content

Commit 296fe6a

Browse files
committed
fix(core): complete goal-driver API sync started in eeeac7e
Adapt the remaining desynced consumers to the restructured goal driver: - goal-tick-port: inline the single-tick body (pause/stop checks + one loop.tick + status publish); the goal-steer relay (makeGoalSteerRelay, GOAL_STEER_DELIVERY, pendingSteer/markSteerConsumed ports) was removed from goal-driver/goal-loop-wiring/core goal-loop, so drop its wiring here and in v4-event-runtime (steerBuffer no longer needed). - multi-agent-runtime: drop the §C1 maxFilesChanged and §E2 maxTokensPerHour gates plus the token-budget tracker — both limit fields were removed from the agent limits schema, making the gates dead code. - Delete test files pinning removed APIs: goal-steer (steer relay), overflow (soft-landing state machine), plan-status-cache (volatile-tail cache design abandoned by the restructure), httpapi-webhook (webhook API dropped in 1db089f); trim removed-API cases from steer, multi-agent-runtime and model-discovery tests; adapt goal-tick-cold-recovery to the single-tick port. typecheck in packages/deepagent-code goes from 75 errors to 0. Four runtime test failures in steer/multi-agent-runtime predate this change (baseline: 8 failures on the same two files) and remain as restructure follow-up.
1 parent 3a530d3 commit 296fe6a

11 files changed

Lines changed: 26 additions & 1701 deletions

packages/deepagent-code/src/session/goal-tick-port.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import type { Session } from "./session"
1414
import type { Agent } from "../agent/agent"
1515
import type { SessionPrompt } from "./prompt"
1616
import type { SessionRevert } from "./revert"
17-
import type { SessionSteer } from "./steer"
1817
import type { Provider } from "../provider/provider"
1918
import type { LSP } from "../lsp/lsp"
2019
import type { RuntimeFlags } from "../effect/runtime-flags"
2120
import { SessionID } from "./schema"
22-
import { GoalDriver, type GoalDriverPorts } from "./goal-driver"
21+
import { type GoalDriverPorts } from "./goal-driver"
2322
import {
2423
GoalLoopWiring,
2524
liveDiagnostics,
@@ -57,7 +56,6 @@ export type GoalTickPortDeps = {
5756
readonly agents: Agent.Interface
5857
readonly sessionPrompt: SessionPrompt.Interface
5958
readonly revert: SessionRevert.Interface
60-
readonly steerBuffer: SessionSteer.Interface
6159
readonly provider: Provider.Interface
6260
readonly lsp: LSP.Interface
6361
readonly instanceStore: InstanceStore.Interface
@@ -182,9 +180,6 @@ export const makeGoalTickPort =
182180
)
183181
const wrappedRollback: typeof rollback = (rbInput) => withContext(rollback(rbInput))
184182

185-
// One goal-steer relay per tick, shared by the wiring (executor threads staged guidance) + the driver.
186-
const steerRelay = GoalDriver.makeGoalSteerRelay()
187-
188183
const deps_ = yield* GoalLoopWiring.makeGoalLoopWiring({
189184
store,
190185
parentSessionID: sessionID,
@@ -193,7 +188,6 @@ export const makeGoalTickPort =
193188
panelQuestion: defaultPanelQuestion,
194189
diagnostics,
195190
rollback: wrappedRollback,
196-
steerRelay,
197191
}).pipe(Effect.provideService(RuntimeFlagsService.Service, deps.flags))
198192
if (deps_ == null) {
199193
log.warn("goal tick: goal loop disabled (experimentalGoalLoop off); halting chain", { sessionID })
@@ -213,34 +207,40 @@ export const makeGoalTickPort =
213207
})
214208

215209
// Ports from DURABLE sources (no in-memory control map on the cold fiber):
216-
// • shouldPause / shouldStop — the session-state active-goal pointer phase (pause/stop persist it).
217-
// • goal-steer — the SessionSteer buffer on the goal session id + goal_steer delivery channel.
210+
// shouldPause / shouldStop read the session-state active-goal pointer phase (pause/stop persist it).
218211
const goalPhase = () => AgentGateway.DeepAgentSessionState.getActiveGoal(sessionID)?.phase
219212
const ports: GoalDriverPorts = {
220213
onStatus: (status) => statusPublisher.publishStatus(sessionID, status),
221214
shouldPause: () => Effect.sync(() => goalPhase() === "paused"),
222215
shouldStop: () => Effect.sync(() => goalPhase() === "stopped"),
223-
pendingSteer: () =>
224-
deps.steerBuffer.pending(SessionID.make(sessionID), GoalDriver.GOAL_STEER_DELIVERY).pipe(
225-
Effect.map((rows) => rows.map((r) => ({ id: r.id, text: r.prompt.text }))),
226-
Effect.catchCause(() => Effect.succeed([] as ReadonlyArray<GoalDriver.PendingGoalSteer>)),
227-
),
228-
markSteerConsumed: (ids) =>
229-
deps.steerBuffer
230-
.markConsumed(SessionID.make(sessionID), [...ids], GoalDriver.GOAL_STEER_DELIVERY)
231-
.pipe(Effect.catchCause(() => Effect.void)),
232216
}
233217

218+
// One tick with the driver's cooperative pause/stop checks (the body of runToCompletion, minus the
219+
// iteration — the event chain re-emits the next command instead of looping in-process).
220+
const loop = makeGoalLoop(deps_)
234221
const handle = { goalId: request.goalId, planDocId: request.planDocId, sessionId: sessionID }
235-
const result = yield* GoalDriver.runOneTick(makeGoalLoop(deps_), { deps: deps_, handle, ports, steerRelay })
222+
const progress = yield* Effect.gen(function* () {
223+
if (yield* ports.shouldStop().pipe(Effect.catchCause(() => Effect.succeed(false)))) {
224+
yield* loop.stop(handle).pipe(Effect.catchCause(() => Effect.void))
225+
return "stopped" as const
226+
}
227+
if (yield* ports.shouldPause().pipe(Effect.catchCause(() => Effect.succeed(false)))) {
228+
return "paused" as const
229+
}
230+
const outcome = yield* loop.tick(handle)
231+
const status = yield* loop.status(handle).pipe(Effect.catchCause(() => Effect.succeed(null)))
232+
if (status) yield* ports.onStatus(status).pipe(Effect.catchCause(() => Effect.void))
233+
if (outcome === "continue") return "continue" as const
234+
return "terminal" as const
235+
})
236236

237237
// The post-tick durable cursor is the next command identity and the retry checkpoint for this command.
238-
// If state vanished, runOneTick cannot continue, so the fallback is unused by the consumer.
238+
// If state vanished, the tick cannot continue, so the fallback is unused by the consumer.
239239
const cursor = readGoalTickCursor(store, sessionID, request.goalId)
240240
const nextSeq = cursor?.seq ?? request.seq + 1
241241
const nextExpectedPlanVersion = cursor?.planVersion ?? request.expectedPlanVersion
242242

243-
return { progress: result.progress, nextSeq, nextExpectedPlanVersion }
243+
return { progress, nextSeq, nextExpectedPlanVersion }
244244
}).pipe(
245245
// The port lives on `never` — a defect here (unexpected) must NOT crash the consumer's stream. But we
246246
// WANT a genuine transient failure to nack for retry, so we RE-RAISE as a die: the consumer's

packages/deepagent-code/src/session/multi-agent-runtime.ts

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,7 @@ export interface LayerOptions {
112112
event: DeepAgentEvent.Event,
113113
files: ReadonlyArray<string>,
114114
) => Effect.Effect<ReadonlyArray<string>>
115-
// §E2 token budget — injectable clock for the per-agent-per-hour LLM token budget's fixed window.
116-
// Defaults to Date.now; tests inject a mutable clock to cross the window boundary deterministically.
117115
readonly now?: () => number
118-
// §E2 token budget window (ms). Defaults to 1 hour — the §E2 "max_tokens_per_hour" cadence.
119-
readonly tokenBudgetWindowMs?: number
120116
}
121117

122118
export const layerWith = (options: LayerOptions) =>
@@ -131,29 +127,6 @@ export const layerWith = (options: LayerOptions) =>
131127
const symbolsForFiles = options.symbolsForFiles
132128
const runner = options.runner
133129
const now = options.now ?? Date.now
134-
const tokenBudgetWindowMs = options.tokenBudgetWindowMs ?? 3_600_000 // 1h — §E2 max_tokens_per_hour
135-
// §E2 LLM token budget — a per-agent fixed-window token accumulator (agentID → {windowStart, used}).
136-
// A subtask is admitted only if the agent is not ALREADY over its declared maxTokensPerHour; after a
137-
// turn we DEBIT the runner's reported tokensUsed. In-memory + process-local (mirrors the bus's
138-
// publishLimiter): a single runtime instance owns it. P4.1 — the production event turn runner now
139-
// threads the REAL per-turn token total (input+output+reasoning) from the prompt result, so this
140-
// budget is LIVE: an agent over maxTokensPerHour genuinely defers. (A stub runner that reports 0 is
141-
// still a harmless no-op debit — the tracker + enforcement are real either way.)
142-
const tokenUsage = new Map<string, { windowStart: number; used: number }>()
143-
const tokensUsedThisHour = (agentID: string, at: number): number => {
144-
const bucket = tokenUsage.get(agentID)
145-
if (!bucket || at - bucket.windowStart >= tokenBudgetWindowMs) return 0
146-
return bucket.used
147-
}
148-
const debitTokens = (agentID: string, tokens: number, at: number): void => {
149-
if (tokens <= 0) return
150-
const bucket = tokenUsage.get(agentID)
151-
if (!bucket || at - bucket.windowStart >= tokenBudgetWindowMs) {
152-
tokenUsage.set(agentID, { windowStart: at, used: tokens })
153-
} else {
154-
bucket.used += tokens
155-
}
156-
}
157130
const trustedSources = options.trustedSources
158131
const trustedSourcesFor = options.trustedSourcesFor
159132
const actorHasPermission = options.actorHasPermission ?? (() => Effect.succeed(true))
@@ -294,19 +267,6 @@ export const layerWith = (options: LayerOptions) =>
294267
continue
295268
}
296269

297-
// §C1 max_files_changed — the agent's declared per-subtask file-scope ceiling. A subtask
298-
// whose declared write scope exceeds it is BLOCKED (terminal, not deferred): the partition's
299-
// fileScope is fixed, so a retry would present the SAME oversized scope — blocking is the
300-
// honest outcome (deferring would spin forever). Unset ⇒ no ceiling. Checked right after the
301-
// bind (it is an agent-vs-subtask fact) and before the autonomy/security gates.
302-
const maxFilesChanged = agent.limits?.maxFilesChanged
303-
if (maxFilesChanged != null && maxFilesChanged >= 0 && subtask.fileScope.length > maxFilesChanged) {
304-
outcomes.push({ taskID: subtask.id, capability: subtask.capability, status: "blocked", agentID: agent.id, reason: "max_files_changed" })
305-
yield* emit(event, { type: "agent.task.blocked", taskID: subtask.id, reason: "max_files_changed" }, `coord:${subtask.id}:blocked`)
306-
// terminal (retrying won't shrink the scope) — do NOT mark hasUnfinished.
307-
continue
308-
}
309-
310270
// §D autonomy gate — the agent's ceiling vs the subtask's required level.
311271
const autonomy = AutonomyPolicy.decide({
312272
agentCeiling: AutonomyPolicy.resolveCeiling(agent),
@@ -389,18 +349,6 @@ export const layerWith = (options: LayerOptions) =>
389349
continue
390350
}
391351
}
392-
// §E2 LLM token budget — a per-agent-per-hour ceiling on tokens consumed. If the agent is
393-
// ALREADY at/over its declared maxTokensPerHour, DEFER this subtask (retryable — the window
394-
// rolls over, unlike max_files_changed which is terminal). Checked before acquiring a slot so
395-
// there is nothing to release on defer. Only bites when a budget is declared AND the runner
396-
// reports real usage (the event turn runner reports 0 today → this never trips there yet).
397-
const maxTokensPerHour = agent.limits?.maxTokensPerHour
398-
if (maxTokensPerHour != null && maxTokensPerHour >= 0 && tokensUsedThisHour(agent.id, now()) >= maxTokensPerHour) {
399-
outcomes.push({ taskID: subtask.id, capability: subtask.capability, status: "deferred", agentID: agent.id, reason: "token_budget_exceeded" })
400-
hasUnfinished = true
401-
continue
402-
}
403-
404352
// §E2 concurrency cap — acquire a per-workspace execution slot. Over cap ⇒ DEFER (retryable
405353
// via the bus, not dropped), so a burst never runs more than the workspace's cap at once.
406354
const slot = concurrency ? yield* concurrency.acquire(event.workspaceID) : undefined
@@ -488,11 +436,6 @@ export const layerWith = (options: LayerOptions) =>
488436
}),
489437
),
490438
)
491-
// §E2 — DEBIT the tokens this turn actually consumed against the agent's per-hour budget, so
492-
// the NEXT subtask this pass (and future events within the window) see the running total. P4.1 —
493-
// the event turn runner now reports the real total, so this debit is live (a stub runner that
494-
// reports 0 is simply a no-op debit).
495-
debitTokens(agent.id, result.tokensUsed, now())
496439

497440
if (result.ok) {
498441
outcomes.push({ taskID: subtask.id, capability: subtask.capability, status: "completed", agentID: agent.id })

packages/deepagent-code/src/session/v4-event-runtime.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import { GoalTickConsumer } from "./goal-tick-consumer"
3939
import { GoalTickPort } from "./goal-tick-port"
4040
import { goalStoreRoot } from "./goal-manager"
4141
import { SessionRevert } from "./revert"
42-
import { SessionSteer } from "./steer"
4342
import { EventV2Bridge } from "@/event-v2-bridge"
4443
// §C3 (P2.9) — file locks + code-graph symbols.
4544
import { FileLock } from "@deepagent-code/core/file-lock"
@@ -697,8 +696,8 @@ const panelConsumerLayer = Layer.unwrap(
697696
// makes the goal-loop tick GENUINELY event-driven with cross-process cold recovery: a goal survives a
698697
// process restart because every tick rebuilds its wiring from the durable run_context doc + the event
699698
// payload (no in-memory control map needed). Draws the SAME session stack makeEventTurnRunner uses, plus
700-
// SessionRevert / SessionSteer / LSP / EventV2Bridge for the rollback / goal-steer / diagnostics / SSE
701-
// ports, all from the shared graph.
699+
// SessionRevert / LSP / EventV2Bridge for the rollback / diagnostics / SSE ports, all from the shared
700+
// graph.
702701
//
703702
// FLAG COUPLING: runLoop = v4MultiAgentRuntime (the master event-driven switch — the goal-manager's
704703
// dual-path start publishes the FIRST command only on this flag). Default posture matches the flag: with
@@ -711,7 +710,6 @@ const goalTickConsumerLayer = Layer.unwrap(
711710
const agents = yield* Agent.Service
712711
const sessionPrompt = yield* SessionPrompt.Service
713712
const revert = yield* SessionRevert.Service
714-
const steerBuffer = yield* SessionSteer.Service
715713
const provider = yield* Provider.Service
716714
const lsp = yield* LSP.Service
717715
const instanceStore = yield* InstanceStore.Service
@@ -723,7 +721,6 @@ const goalTickConsumerLayer = Layer.unwrap(
723721
agents,
724722
sessionPrompt,
725723
revert,
726-
steerBuffer,
727724
provider,
728725
lsp,
729726
instanceStore,

0 commit comments

Comments
 (0)