feat(x/core): turn runtime with event-sourced execution loop (stage 2)

The TurnRuntime per turn-runtime-design.md: constructor-injected,
container-ignorant, no per-turn state — every advanceTurn reconstructs
from the JSONL log, so normal execution and crash recovery share one
path.

- runtime.ts: createTurn/advanceTurn/getTurn; durable-barrier appends
  before side effects; sequential sync tools with progress; async tool
  exposure + durable suspension; permission pipeline (checker →
  optional classifier batch → human/deny fallback, fail-closed on
  checker errors); cancellation with synthetic results; recovery that
  re-issues interrupted model calls and continues after interrupted
  sync tools; model-call-limit exhaustion with machine-readable code.
- fs-repo.ts: date-partitioned append-only JSONL, strict line
  validation, path-traversal rejection, per-turn in-process locking.
- stream.ts: hot execution stream — buffers before the consumer
  attaches, outcome independent of consumption, close-drops-events.
- context-resolver.ts: chain-walking materialization of context
  references with cycle detection.
- Seam interfaces for stage-4 bridges: model/tool registries,
  permission checker/classifier, agent resolver, lifecycle bus, clock.
- shared reducer: track classification failures durably so failed
  classifications are never re-run.

67 new tests: all nine §26 end-to-end scenarios (mocked deps,
recovery boundaries seeded directly) plus repo/stream/resolver suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-02 09:32:27 +05:30
parent d821c419fe
commit cda17c2d40
21 changed files with 3656 additions and 1 deletions

View file

@ -506,6 +506,7 @@ describe("tool execution", () => {
permResolved("tc1", "deny", "human"),
result("tc1", "echo", "runtime", "Permission denied", true),
]);
expect(state.toolCalls[0].permission?.classificationFailed).toBe(true);
expect(state.toolCalls[0].result?.result.isError).toBe(true);
});

View file

@ -382,6 +382,9 @@ export interface ToolCallState {
permission?: {
required: z.infer<typeof ToolPermissionRequired>;
classification?: z.infer<typeof ToolPermissionClassified>;
// Set when a tool_permission_classification_failed event named this
// call; such calls are treated as defer and never re-classified.
classificationFailed?: boolean;
resolved?: z.infer<typeof ToolPermissionResolved>;
};
invocation?: z.infer<typeof ToolInvocationRequested>;
@ -635,8 +638,8 @@ function applyToolPermissionClassificationFailed(
if (toolCall.permission.resolved) {
fail(`classification failure after permission resolution: ${toolCallId}`);
}
toolCall.permission.classificationFailed = true;
}
// Recorded for the audit trail; does not currently affect derived state.
}
function applyToolPermissionResolved(