Skip to content

Commit b64cc80

Browse files
fix(core): wait for plugin activation before each runner step (anomalyco#51133)
Co-authored-by: Daniel Smolsky <[email protected]>
1 parent 7d3dd86 commit b64cc80

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

‎packages/core/src/session/runner/llm.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ const layer = Layer.effect(
114114
if (pending?.type === "compaction") {
115115
const compacted = yield* restore(
116116
Effect.gen(function* () {
117+
yield* plugins.awaitActivation
117118
const selected = yield* context.select(sessionID)
118119
const model = yield* context.resolveModel(selected.session)
119120
// Preview updates without admitting them after the already-delivered compaction marker.
@@ -193,6 +194,8 @@ const layer = Layer.effect(
193194
})
194195

195196
const prepareContext = Effect.fn("SessionRunner.prepareContext")(function* (sessionID: SessionSchema.ID) {
197+
// Plugins can reload mid-turn; each step and retry must read the settled agents, models and skills.
198+
yield* plugins.awaitActivation
196199
const selected = yield* context.select(sessionID)
197200
// A blocked initial instruction baseline must leave admitted input pending.
198201
yield* InstructionState.prepare(db, bus, selected.instructions, sessionID)
@@ -246,7 +249,11 @@ const layer = Layer.effect(
246249
assistantMessageID,
247250
agent: loaded.agent.id,
248251
model: loaded.model,
249-
prepared,
252+
prepared: {
253+
...prepared,
254+
// A tool call can arrive during a reload; its permission check reads the session's agent.
255+
executeTool: (call) => plugins.awaitActivation.pipe(Effect.andThen(prepared.executeTool(call))),
256+
},
250257
retry: (cause, error, proposed) =>
251258
retry.decide({
252259
cause,

‎packages/core/test/session-runner.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ const makeRunnerState = (compaction?: SessionRunnerModel.Resolved["compaction"])
215215
systemRemoved: false,
216216
systemUnavailable: false,
217217
systemLoadHook: Effect.void,
218+
activationHook: Effect.void,
218219
skillBaselines: new Map<Agent.ID, string>(),
219220
authorizations: new Array<Tool.Context>(),
220221
executions: new Array<string>(),
@@ -422,7 +423,7 @@ const layer = Layer.unwrap(
422423
Permission.node.replace(permission),
423424
Config.node.replace(config),
424425
PluginSupervisor.node.replace(Layer.empty),
425-
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.void })),
426+
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.suspend(() => state.activationHook) })),
426427
SessionModelTransport.node.replace(modelTransport),
427428
]
428429
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
@@ -2664,6 +2665,26 @@ describe("SessionRunnerLLM", () => {
26642665
}
26652666
}
26662667

2668+
scenario("waits for plugin activation before each step and tool call", function* (s) {
2669+
const waits: Array<[number, number]> = []
2670+
s.activationHook = Effect.sync(() => {
2671+
waits.push([s.requests.length, s.executions.length])
2672+
})
2673+
yield* s.llm.push(
2674+
TestLLM.tool("call-activation", "echo", { text: "echo" }),
2675+
TestLLM.text("Done", "text-after-activation"),
2676+
)
2677+
yield* s.runPrompt("Question")
2678+
2679+
// Model requests and tool executions seen at each wait: drain entry, step 1, the tool call, step 2.
2680+
expect(waits).toEqual([
2681+
[0, 0],
2682+
[0, 0],
2683+
[1, 0],
2684+
[1, 1],
2685+
])
2686+
})
2687+
26672688
scenario("restarts compaction drafts after transient failures and unsuccessful finishes", function* (s) {
26682689
yield* s.llm.push(TestLLM.text("Earlier answer", "text-manual-failure-history"))
26692690
yield* s.runPrompt("Earlier question")

0 commit comments

Comments
 (0)