@xandreed/core 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.functions.d.ts +34 -0
  3. package/dist/conformance.functions.js +25 -0
  4. package/dist/domain/bash.entity.d.ts +82 -0
  5. package/dist/domain/bash.entity.functions.d.ts +17 -0
  6. package/dist/domain/bash.entity.functions.js +26 -0
  7. package/dist/domain/bash.entity.functions.test.d.ts +1 -0
  8. package/dist/domain/bash.entity.js +30 -0
  9. package/dist/domain/file-system.entity.d.ts +91 -0
  10. package/dist/domain/file-system.entity.functions.d.ts +18 -0
  11. package/dist/domain/file-system.entity.functions.js +30 -0
  12. package/dist/domain/file-system.entity.js +37 -0
  13. package/dist/domain/loop-event.entity.d.ts +9 -0
  14. package/dist/domain/media.entity.d.ts +79 -0
  15. package/dist/domain/media.entity.functions.d.ts +76 -0
  16. package/dist/domain/media.entity.functions.js +110 -0
  17. package/dist/domain/media.entity.js +38 -0
  18. package/dist/domain/media.entity.test.d.ts +1 -0
  19. package/dist/domain/message.entity.d.ts +157 -5
  20. package/dist/domain/message.entity.js +8 -4
  21. package/dist/domain/model-catalog.entity.d.ts +2 -0
  22. package/dist/domain/model-catalog.entity.js +2 -1
  23. package/dist/domain/token-usage.entity.d.ts +45 -1
  24. package/dist/domain/token-usage.entity.functions.d.ts +30 -1
  25. package/dist/domain/token-usage.entity.functions.js +24 -4
  26. package/dist/domain/token-usage.entity.js +19 -2
  27. package/dist/domain/token-usage.entity.test.d.ts +1 -0
  28. package/dist/harness/capability.entity.functions.js +2 -1
  29. package/dist/harness/plugin.adapter.d.ts +3 -0
  30. package/dist/harness/plugin.adapter.js +7 -1
  31. package/dist/harness/skill-files.entity.d.ts +101 -0
  32. package/dist/harness/skill-files.entity.functions.d.ts +58 -0
  33. package/dist/harness/skill-files.entity.functions.js +150 -0
  34. package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
  35. package/dist/harness/skill-files.entity.js +25 -0
  36. package/dist/index.d.ts +48 -8
  37. package/dist/index.js +67 -6
  38. package/dist/loop/mapping.d.ts +12 -6
  39. package/dist/loop/mapping.js +30 -6
  40. package/dist/loop/stepContext.d.ts +8 -0
  41. package/dist/loop/stepContext.js +1 -1
  42. package/dist/loop/traceContent.d.ts +29 -0
  43. package/dist/loop/traceContent.js +4 -0
  44. package/dist/loop/traceContent.test.d.ts +1 -0
  45. package/dist/memory/memory-log.entity.d.ts +152 -14
  46. package/dist/memory/memory-log.entity.functions.d.ts +35 -5
  47. package/dist/memory/memory-log.entity.functions.js +59 -20
  48. package/dist/memory/memory-log.entity.js +19 -6
  49. package/dist/memory/memory-session.js +10 -6
  50. package/dist/memory/memory.conformance.d.ts +8 -1
  51. package/dist/memory/memory.conformance.js +32 -0
  52. package/dist/models/auth-store.conformance.d.ts +13 -0
  53. package/dist/models/auth-store.conformance.js +73 -0
  54. package/dist/models/auth-store.conformance.test.d.ts +1 -0
  55. package/dist/models/model-catalog.conformance.d.ts +15 -0
  56. package/dist/models/model-catalog.conformance.js +27 -0
  57. package/dist/models/model-catalog.conformance.test.d.ts +1 -0
  58. package/dist/models/settings-store.conformance.d.ts +26 -0
  59. package/dist/models/settings-store.conformance.js +80 -0
  60. package/dist/models/settings-store.conformance.test.d.ts +1 -0
  61. package/dist/ports/activity.port.d.ts +35 -0
  62. package/dist/ports/activity.port.js +7 -0
  63. package/dist/ports/auth-store.port.d.ts +7 -5
  64. package/dist/ports/bash.port.d.ts +22 -0
  65. package/dist/ports/bash.port.js +4 -0
  66. package/dist/ports/capability.port.d.ts +11 -1
  67. package/dist/ports/file-system.port.d.ts +44 -15
  68. package/dist/ports/file-system.port.js +1 -7
  69. package/dist/ports/memory.port.d.ts +13 -1
  70. package/dist/ports/model-catalog.port.d.ts +4 -1
  71. package/dist/ports/run-context.port.d.ts +6 -1
  72. package/dist/ports/session-log.port.d.ts +4 -1
  73. package/dist/ports/sessions.port.d.ts +35 -6
  74. package/dist/ports/settings-store.port.d.ts +11 -7
  75. package/dist/ports/step-loop.port.d.ts +30 -2
  76. package/dist/ports/tasks.port.d.ts +5 -1
  77. package/dist/ports/turn-admission.port.d.ts +8 -2
  78. package/dist/ports/turn-events.port.d.ts +8 -1
  79. package/dist/ports/turn-scope.port.d.ts +19 -1
  80. package/dist/ports/turn.port.d.ts +70 -5
  81. package/dist/ports/utility-llm.port.d.ts +2 -0
  82. package/dist/session/activity.entity.d.ts +42 -0
  83. package/dist/session/activity.entity.functions.d.ts +21 -0
  84. package/dist/session/activity.entity.functions.js +23 -0
  85. package/dist/session/activity.entity.functions.test.d.ts +1 -0
  86. package/dist/session/activity.entity.js +15 -0
  87. package/dist/session/chassis.js +2 -1
  88. package/dist/session/session-event.entity.d.ts +68 -6
  89. package/dist/session/session-event.entity.functions.d.ts +34 -2
  90. package/dist/session/session-event.entity.functions.js +54 -16
  91. package/dist/session/session-event.entity.js +19 -2
  92. package/dist/session/session-log.conformance.d.ts +4 -3
  93. package/dist/session/session-log.conformance.js +16 -14
  94. package/dist/session/session-log.recovery.conformance.d.ts +40 -0
  95. package/dist/session/session-log.recovery.conformance.js +143 -0
  96. package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
  97. package/dist/session/sessions.conformance.d.ts +79 -0
  98. package/dist/session/sessions.conformance.js +355 -0
  99. package/dist/session/sessions.entity.d.ts +36 -3
  100. package/dist/session/sessions.entity.functions.d.ts +42 -0
  101. package/dist/session/sessions.entity.functions.js +26 -0
  102. package/dist/session/sessions.entity.js +17 -4
  103. package/dist/session/sessions.entity.test.d.ts +1 -0
  104. package/dist/session/tasks.conformance.d.ts +70 -0
  105. package/dist/session/tasks.conformance.js +213 -0
  106. package/dist/session/tasks.conformance.test.d.ts +1 -0
  107. package/dist/turn/model-request.entity.d.ts +4 -0
  108. package/dist/turn/model-request.entity.js +7 -2
  109. package/dist/turn/step-loop.conformance.d.ts +4 -1
  110. package/dist/turn/step-loop.conformance.js +73 -7
  111. package/dist/turn/tool-registry.conformance.d.ts +38 -0
  112. package/dist/turn/tool-registry.conformance.js +206 -0
  113. package/dist/turn/turn-bus.js +4 -3
  114. package/dist/turn/turn-event.entity.d.ts +134 -34
  115. package/dist/turn/turn-event.entity.functions.d.ts +11 -0
  116. package/dist/turn/turn-event.entity.functions.js +9 -1
  117. package/dist/turn/turn-event.entity.js +76 -3
  118. package/dist/turn/turn-event.entity.test.d.ts +1 -0
  119. package/dist/turn/turn-lifecycle.d.ts +46 -8
  120. package/dist/turn/turn-lifecycle.js +33 -3
  121. package/dist/turn/turn-lifecycle.test.d.ts +1 -0
  122. package/dist/turn/turn-run.d.ts +15 -3
  123. package/dist/turn/turn-run.js +37 -9
  124. package/dist/turn/turn.adapter.d.ts +6 -5
  125. package/dist/turn/turn.adapter.js +15 -3
  126. package/dist/turn/turn.conformance.d.ts +11 -2
  127. package/dist/turn/turn.conformance.js +63 -2
  128. package/dist/turn/user-message.entity.d.ts +14 -4
  129. package/dist/turn/user-message.entity.functions.d.ts +21 -0
  130. package/dist/turn/user-message.entity.functions.js +24 -0
  131. package/dist/turn/user-message.entity.js +6 -8
  132. package/dist/turn/user-message.entity.test.d.ts +1 -0
  133. package/dist/util/glob.d.ts +30 -0
  134. package/dist/util/glob.js +91 -0
  135. package/dist/util/glob.test.d.ts +1 -0
  136. package/dist/workspace/bash.conformance.d.ts +37 -0
  137. package/dist/workspace/bash.conformance.js +120 -0
  138. package/dist/workspace/bash.conformance.test.d.ts +1 -0
  139. package/dist/workspace/file-system.conformance.d.ts +38 -0
  140. package/dist/workspace/file-system.conformance.js +254 -0
  141. package/dist/workspace/file-system.conformance.test.d.ts +1 -0
  142. package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
  143. package/dist/workspace/file-system.memory.adapter.js +132 -0
  144. package/package.json +1 -1
  145. package/dist/harness/skill-files.d.ts +0 -17
  146. package/dist/harness/skill-files.js +0 -25
  147. package/dist/ports/shell.port.d.ts +0 -32
  148. package/dist/ports/shell.port.js +0 -16
@@ -0,0 +1,206 @@
1
+ import { Tool } from "effect/ai";
2
+ import { Context, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
3
+ import { check, expect, freshId } from "../conformance.functions.js";
4
+ import { Failure } from "../domain/failure.entity.js";
5
+ import { defineCapability, defineSkill, defineTool } from "../harness/capability.entity.functions.js";
6
+ import { HarnessError } from "../harness/plugin.entity.js";
7
+ import { CurrentAgentStep, CurrentToolInvocation } from "../loop/stepContext.js";
8
+ import { EntryId } from "../memory/memory-log.entity.js";
9
+ import { openLogSession } from "../memory/memory-session.js";
10
+ import { ActionPolicy } from "../ports/harness.port.js";
11
+ import { ConversationMemory } from "../ports/memory.port.js";
12
+ import { PermissionGrants } from "../ports/permission.port.js";
13
+ import { RunContext } from "../ports/run-context.port.js";
14
+ import { ToolRegistry } from "../ports/tool-registry.port.js";
15
+ import { TurnMemory, TurnToolbox } from "../ports/turn-scope.port.js";
16
+ import { turnStartedDraft } from "../session/session-event.entity.functions.js";
17
+ import { TurnLive } from "./turn.adapter.js";
18
+ import { publishToolProgress } from "./turn-event.entity.functions.js";
19
+ import { UserMessage } from "./user-message.entity.js";
20
+ const RUN = "conformance-run", Opener = Context.Reference("efferent/conformance/ToolRegistryOpener", { defaultValue: () => "none" }), Ping = Tool.make("kit_ping", {
21
+ description: "Answer pong, at most twice per turn.",
22
+ parameters: Schema.Struct({ n: Schema.Int }),
23
+ success: Schema.String,
24
+ failure: Failure,
25
+ failureMode: "return"
26
+ }), Report = Tool.make("kit_report", {
27
+ description: "Report progress twice, then say which call this was.",
28
+ parameters: Schema.Struct({}),
29
+ success: Schema.String,
30
+ failure: Failure,
31
+ failureMode: "return"
32
+ }), Vault = Tool.make("kit_vault", {
33
+ description: "Read the vault, with the kit.vault permission.",
34
+ parameters: Schema.Struct({ key: Schema.String }),
35
+ success: Schema.String,
36
+ failure: Failure,
37
+ failureMode: "return"
38
+ }), Archive = Tool.make("kit_archive", {
39
+ description: "Archive a record, once its skill is loaded.",
40
+ parameters: Schema.Struct({ id: Schema.String }),
41
+ success: Schema.String,
42
+ failure: Failure,
43
+ failureMode: "return"
44
+ }), TOOLS = [Ping.name, Report.name, Vault.name, Archive.name], SKILLS = ["kit.basics", "kit.vault", "kit.archiving"], capabilityOf = (ran) => {
45
+ const note = (tool) => Ref.update(ran, (all) => [...all, tool]);
46
+ return defineCapability({
47
+ id: "conformance.tools",
48
+ version: "1",
49
+ tools: [
50
+ defineTool({ tool: Ping, handler: ({ n }) => note(Ping.name).pipe(Effect.as(`pong ${n}`)), annotations: { maxCallsPerRun: Option.some(2) } }),
51
+ defineTool({
52
+ tool: Report,
53
+ handler: () => Effect.gen(function* () {
54
+ yield* note(Report.name);
55
+ const invocation = yield* Effect.service(CurrentToolInvocation), opener = yield* Effect.service(Opener);
56
+ yield* publishToolProgress("stdout", "working");
57
+ yield* publishToolProgress("status", "half way");
58
+ return `${Option.match(invocation, { onNone: () => "no invocation", onSome: (call) => `${call.invocationId}|${call.tool}|${call.step}` })}|${opener}`;
59
+ })
60
+ }),
61
+ defineTool({ tool: Vault, handler: ({ key }) => note(Vault.name).pipe(Effect.as(`secret ${key}`)), annotations: { permissions: ["kit.vault"] } }),
62
+ defineTool({ tool: Archive, handler: ({ id }) => note(Archive.name).pipe(Effect.as(`archived ${id}`)) })
63
+ ],
64
+ skills: [
65
+ defineSkill({ id: "kit.basics", summary: "Ping and report.", tools: [Ping.name, Report.name], always: !0 }),
66
+ defineSkill({ id: "kit.vault", summary: "Read the vault.", tools: [Vault.name], permissions: ["kit.vault"] }),
67
+ defineSkill({ id: "kit.archiving", summary: "Archive records.", tools: [Archive.name] })
68
+ ]
69
+ });
70
+ }, policy = {
71
+ strategy: { id: "conformance", version: "1" },
72
+ render: { turnContext: "current", replies: !0, digests: !0, media: { mode: "none", maxImages: 0, maxChars: 0 } },
73
+ digestOnWrite: Option.none(),
74
+ maintain: () => Effect.succeed({ actions: [], digest: [] })
75
+ }, memoryWriter = Effect.gen(function* () {
76
+ const session = yield* freshId, userMessage = new UserMessage({ text: "use the tools" }), draft = yield* turnStartedDraft(1, {
77
+ runId: RUN,
78
+ key: RUN,
79
+ origin: "user",
80
+ userMessage,
81
+ command: {},
82
+ claimed: [],
83
+ entry: EntryId.make(`${RUN}:0`),
84
+ at: 0
85
+ }).pipe(Effect.orDie), started = { session, seq: 1, turn: draft.turn, kind: draft.kind, at: 0, data: draft.data }, stored = yield* Ref.make([started]), append = (drafts) => Ref.update(stored, (all) => [...all, ...drafts.map((next, index) => ({
86
+ session,
87
+ seq: all.length + index + 1,
88
+ turn: Option.some(1),
89
+ kind: next.kind,
90
+ at: 0,
91
+ data: JSON.parse(JSON.stringify(next.data))
92
+ }))]);
93
+ return { writer: {
94
+ admitted: { session: { id: session, owner: "conformance" }, turn: 1, runId: RUN, key: RUN, origin: "user", userMessage, command: {}, claimed: [], meta: {} },
95
+ started,
96
+ history: () => Effect.succeed([]),
97
+ snapshot: (kinds, after) => Ref.get(stored).pipe(Effect.map((events) => events.filter((event) => (after === void 0 || event.seq > after) && (kinds.length === 0 || kinds.includes(event.kind))))),
98
+ append,
99
+ write: (op) => op,
100
+ transact: (decide) => decide([]).pipe(Effect.flatMap((decision) => append(decision.drafts).pipe(Effect.as({ result: decision.result, events: [] })))),
101
+ flush: Effect.void,
102
+ end: () => Effect.succeed({ pending: 0 }),
103
+ closed: Effect.never
104
+ }, stored: Ref.get(stored) };
105
+ }), messageOf = (result) => typeof result === "object" && result !== null && ("message" in result) && typeof result.message === "string" ? result.message : "none";
106
+ export const toolRegistryConformance = (make) => {
107
+ const inTurn = (setup, body) => Effect.scoped(Effect.gen(function* () {
108
+ const ran = yield* Ref.make([]), registry = yield* make([capabilityOf(ran)], { maxCallsPerRun: setup.maxCallsPerRun ?? 32 }), recorded = yield* memoryWriter, services = Layer.merge(Layer.succeed(ConversationMemory, ConversationMemory.of({ strategy: policy.strategy, open: ({ runId, log }) => openLogSession(log, policy, { runId }) })), Layer.succeed(ToolRegistry, registry));
109
+ return yield* Effect.gen(function* () {
110
+ const run = yield* RunContext, published = yield* Ref.make([]);
111
+ yield* run.events.subscribe((event) => event._tag.startsWith("tool.") || event._tag === "skills.activated" ? Option.some(event) : Option.none(), (event) => Ref.update(published, (all) => [...all, event]));
112
+ const memory = yield* TurnMemory;
113
+ yield* memory.persistMessage;
114
+ if (setup.vaultActive === !0)
115
+ yield* memory.session.record([{ _tag: "ToolsActivated", skills: ["kit.vault"], tools: [Vault.name], source: "host", decision: Option.none() }], 0);
116
+ const tools = yield* (yield* TurnToolbox).open.pipe(Effect.provideService(Opener, "the opener's"), (open) => Option.match(Option.fromNullishOr(setup.policy), { onNone: () => open, onSome: (denying) => open.pipe(Effect.provideService(ActionPolicy, denying)) }), (open) => Option.match(Option.fromNullishOr(setup.grants), {
117
+ onNone: () => open,
118
+ onSome: (grants) => open.pipe(Effect.provideService(PermissionGrants, PermissionGrants.of({ grants: Effect.succeed(new Set(grants)) })))
119
+ }));
120
+ yield* tools.select(run.userMessage);
121
+ const call = (name, params, step = 1) => Effect.gen(function* () {
122
+ const kit = yield* tools.toolkit.pipe(Effect.provide(tools.handlers)), last = (yield* Stream.runCollect(yield* kit.handle(name, params))).at(-1);
123
+ return { isFailure: last?.isFailure ?? !0, result: last?.result };
124
+ }).pipe(Effect.provideService(CurrentAgentStep, Option.some(step)), Effect.provideService(Opener, "the caller's"));
125
+ return yield* body({
126
+ catalog: registry.catalog,
127
+ tools,
128
+ call,
129
+ published: run.flush.pipe(Effect.orDie, Effect.andThen(Ref.get(published))),
130
+ stored: run.flush.pipe(Effect.orDie, Effect.andThen(recorded.stored)),
131
+ ran: Ref.get(ran)
132
+ });
133
+ }).pipe(Effect.provide(TurnLive({ turn: recorded.writer })), Effect.provide(services));
134
+ })), tags = (events) => events.map((event) => event._tag).join(), idOf = (event) => ("invocationId" in event) ? event.invocationId : "", refusal = (reason, id, calls, setup = {}) => check(`a call refused as ${reason} takes its invocation id first, publishes one tool.refused and no tool.started on the run's bus only, and never runs the handler`, id, () => inTurn(setup, (turn) => Effect.gen(function* () {
135
+ const refusedAt = calls.length - 1, [tool, input] = calls[refusedAt], results = yield* Effect.forEach(calls, ([name, params]) => turn.call(name, params)), events = yield* turn.published, refused = events.flatMap((event) => event._tag === "tool.refused" ? [event] : []);
136
+ yield* expect(id, refused.length === 1, `the calls published ${refused.length} tool.refused`);
137
+ const event = refused[0];
138
+ yield* expect(id, event.reason === reason && event.tool === tool && event.step === 1 && JSON.stringify(event.input) === JSON.stringify(input), `the refusal was published as ${event.reason} of ${event.tool} at step ${event.step} with input ${JSON.stringify(event.input)}`);
139
+ yield* expect(id, event.invocationId === `${RUN}:call:${refusedAt}`, `the refused call's id is ${event.invocationId}, not ${RUN}:call:${refusedAt} (ids are minted before the checks)`);
140
+ const outcome = results[refusedAt];
141
+ yield* expect(id, outcome.isFailure && messageOf(outcome.result) === event.message && event.message.length > 0, `the refused call returned ${JSON.stringify(outcome.result)} (failure: ${outcome.isFailure}), not the refusal's message "${event.message}"`);
142
+ const others = events.filter((other) => other._tag !== "tool.refused" && idOf(other) === event.invocationId);
143
+ yield* expect(id, others.length === 0, `the refused call also published ${tags(others)}`);
144
+ const ran = yield* turn.ran;
145
+ yield* expect(id, ran.length === refusedAt, `the handlers ran for ${ran.join() || "no call"}; only the ${refusedAt} calls before the refused one should have`);
146
+ const stored = (yield* turn.stored).filter((record) => record.kind === "tool.refused");
147
+ yield* expect(id, stored.length === 1 && stored[0].data.invocationId === event.invocationId && !("input" in stored[0].data), `the turn stored ${stored.length} tool.refused${stored.length > 0 && "input" in stored[0].data ? ", with the call's input" : ""} (a registry publishes on the run's bus and never writes the journal itself)`);
148
+ })));
149
+ return [
150
+ check("the catalog and a run's tools hold every tool and skill the capabilities define, with their permissions", "catalog", () => inTurn({}, (turn) => Effect.gen(function* () {
151
+ const recipes = turn.catalog.recipes.map((recipe) => recipe.id), listed = turn.catalog.tools.map((tool) => tool.id);
152
+ yield* expect("catalog", SKILLS.every((skill) => recipes.includes(skill)) && TOOLS.every((tool) => listed.includes(tool)), `the catalog lists the skills ${recipes.join()} and the tools ${listed.join()}`);
153
+ const vault = turn.catalog.tools.find((tool) => tool.id === Vault.name);
154
+ yield* expect("catalog", vault?.permissions.join() === "kit.vault", `the catalog gives ${Vault.name} the permissions [${vault?.permissions.join() ?? "of no entry"}]`);
155
+ const skills = turn.tools.skills.map((skill) => skill.id);
156
+ yield* expect("catalog", SKILLS.every((skill) => skills.includes(skill)), `the run's skills are ${skills.join()}`);
157
+ const toolkit = Object.keys(turn.tools.toolkit.tools);
158
+ yield* expect("catalog", TOOLS.every((tool) => toolkit.includes(tool)), `the run's toolkit holds ${toolkit.join()}`);
159
+ }))),
160
+ check("always-on skills are active, others only once activated; an unknown skill is refused and an ungranted one stays inactive; the active set only grows", "activation", () => inTurn({}, (turn) => Effect.gen(function* () {
161
+ const active = yield* turn.tools.active;
162
+ yield* expect("activation", active.includes(Ping.name) && active.includes(Report.name) && !active.includes(Archive.name) && !active.includes(Vault.name), `with only the always-on skill applied, the active tools are ${active.join()}`);
163
+ const returned = yield* turn.tools.activate(["kit.archiving"], "host"), grown = yield* turn.tools.active;
164
+ yield* expect("activation", returned.includes(Archive.name) && grown.includes(Archive.name) && active.every((tool) => grown.includes(tool)), `after activating kit.archiving the active tools are ${grown.join()} (activate returned ${returned.join()})`);
165
+ yield* turn.tools.activate(["kit.archiving"], "host");
166
+ const again = yield* turn.tools.active;
167
+ yield* expect("activation", again.join() === grown.join(), `activating kit.archiving again changed the active tools to ${again.join()}`);
168
+ const activated = (yield* turn.published).flatMap((event) => event._tag === "skills.activated" ? [event] : []);
169
+ yield* expect("activation", activated.filter((event) => event.skills.includes("kit.archiving")).length === 1 && activated.some((event) => event.tools.includes(Archive.name)), `kit.archiving's activation published ${activated.length} skills.activated`);
170
+ const unknown = yield* Effect.flip(turn.tools.activate(["kit.no-such-skill"], "host"));
171
+ yield* expect("activation", unknown.code === "skills.unknown", `activating an unknown skill failed with ${unknown.code}`);
172
+ yield* Effect.result(turn.tools.activate(["kit.vault"], "host"));
173
+ const after = yield* turn.tools.active;
174
+ yield* expect("activation", !after.includes(Vault.name) && grown.every((tool) => after.includes(tool)), `after asking for a skill whose permission is not granted the active tools are ${after.join()}`);
175
+ }))),
176
+ check("a handled call publishes tool.started, its progress and tool.completed under one invocation id, minted after a refused call's, which its handler sees, on the run's bus only", "invocation", () => inTurn({}, (turn) => Effect.gen(function* () {
177
+ yield* turn.call(Archive.name, { id: "r0" });
178
+ const outcome = yield* turn.call(Report.name, {}, 2), id = `${RUN}:call:1`;
179
+ yield* expect("invocation", !outcome.isFailure && outcome.result === `${id}|${Report.name}|2|the opener's`, `the handler saw ${JSON.stringify(outcome.result)}, not its call ${id}|${Report.name}|2 and the services where the tools were opened`);
180
+ const events = (yield* turn.published).filter((event) => event._tag.startsWith("tool."));
181
+ yield* expect("invocation", tags(events) === "tool.refused,tool.started,tool.progress,tool.progress,tool.completed", `the two calls published ${tags(events)}`);
182
+ const handled = events.slice(1);
183
+ yield* expect("invocation", idOf(events[0]) === `${RUN}:call:0` && handled.every((event) => idOf(event) === id && ("tool" in event) && event.tool === Report.name && ("step" in event) && event.step === 2), `the calls' events carry the ids ${events.map(idOf).join()}`);
184
+ const completed = events.at(-1);
185
+ yield* expect("invocation", completed?._tag === "tool.completed" && completed.ok, "the handled call did not complete ok");
186
+ const stored = (yield* turn.stored).map((event) => event.kind).filter((kind) => kind.startsWith("tool."));
187
+ yield* expect("invocation", stored.join() === "tool.refused,tool.started,tool.completed", `the turn stored ${stored.join()} (a registry publishes on the run's bus and never writes the journal itself)`);
188
+ const next = yield* turn.call(Ping.name, { n: 1 }), started = (yield* turn.published).filter((event) => event._tag === "tool.started").map(idOf);
189
+ yield* expect("invocation", !next.isFailure && started.join() === `${id},${RUN}:call:2`, `the next call took the id ${started.at(-1) ?? "none"}`);
190
+ }))),
191
+ check("the turn's PermissionGrants, read where the tools are opened, let a skill that needs them be activated and its tools run", "grants", () => inTurn({ grants: ["kit.vault"] }, (turn) => Effect.gen(function* () {
192
+ yield* turn.tools.activate(["kit.vault"], "host");
193
+ const active = yield* turn.tools.active;
194
+ yield* expect("grants", active.includes(Vault.name), `with kit.vault granted, activating its skill left the active tools ${active.join()}`);
195
+ const outcome = yield* turn.call(Vault.name, { key: "k" });
196
+ yield* expect("grants", !outcome.isFailure && outcome.result === "secret k", `the granted call returned ${JSON.stringify(outcome.result)}`);
197
+ }))),
198
+ refusal("inactive", "refused-inactive", [[Archive.name, { id: "r1" }]]),
199
+ refusal("forbidden", "refused-forbidden", [[Vault.name, { key: "k" }]], { vaultActive: !0 }),
200
+ refusal("denied", "refused-denied", [[Ping.name, { n: 1 }]], {
201
+ policy: ActionPolicy.of({ authorize: (tool) => tool === Ping.name ? Effect.fail(new HarnessError({ code: "conformance.denied", message: "pings are closed today" })) : Effect.void })
202
+ }),
203
+ refusal("call-limit", "refused-call-limit", [[Ping.name, { n: 1 }], [Ping.name, { n: 2 }]], { maxCallsPerRun: 1 }),
204
+ refusal("tool-call-limit", "refused-tool-call-limit", [[Ping.name, { n: 1 }], [Ping.name, { n: 2 }], [Ping.name, { n: 3 }]])
205
+ ];
206
+ };
@@ -1,5 +1,6 @@
1
1
  import { Cause, Context, Deferred, Effect, Fiber, Option, Queue, Ref } from "effect";
2
2
  import { HarnessError } from "../harness/plugin.entity.js";
3
+ import { CurrentTraceContent, replayableContext } from "../loop/traceContent.js";
3
4
  const failureOf = (cause) => Option.getOrElse(Cause.findErrorOption(cause), () => {
4
5
  const defect = Cause.squash(cause);
5
6
  return defect instanceof HarnessError ? defect : new HarnessError({ code: "events.background", message: Cause.pretty(cause).slice(0, 300) });
@@ -20,7 +21,7 @@ export const makeTurnEvents = (options) => Effect.gen(function* () {
20
21
  }
21
22
  if (Option.isSome(failure))
22
23
  return;
23
- yield* handle(item.event).pipe(Effect.provideService(depth, item.depth), Effect.catchCause((cause) => Ref.set(failed, Option.some(failureOf(cause)))));
24
+ yield* handle(item.event).pipe(Effect.provideService(depth, item.depth), Effect.provideService(CurrentTraceContent, item.traceContent), Effect.catchCause((cause) => Ref.set(failed, Option.some(failureOf(cause)))));
24
25
  })));
25
26
  yield* Effect.forkScoped(Effect.forever(take));
26
27
  const deliver = (event) => Effect.gen(function* () {
@@ -28,11 +29,11 @@ export const makeTurnEvents = (options) => Effect.gen(function* () {
28
29
  if (Option.isSome(failure))
29
30
  return yield* Effect.fail(failure.value);
30
31
  yield* Ref.update(queued, (value) => value + 1);
31
- yield* Queue.offer(queue, { _tag: "Event", event, depth: yield* Effect.service(depth) });
32
+ yield* Queue.offer(queue, { _tag: "Event", event, depth: yield* Effect.service(depth), traceContent: yield* Effect.service(CurrentTraceContent) });
32
33
  }), drain = Deferred.make().pipe(Effect.flatMap((done) => Queue.offer(queue, { _tag: "Marker", done }).pipe(Effect.andThen(Deferred.await(done)))));
33
34
  return { deliver, drain };
34
35
  }), subscribe = (select, handle, subscribeOptions) => Effect.gen(function* () {
35
- const context = yield* Effect.context(), id = yield* Ref.getAndUpdate(nextId, (value) => value + 1), handled = (selected) => handle(selected).pipe(Effect.provide(context)), lane = (subscribeOptions?.mode ?? "inline") === "background" ? yield* background(handled, subscribeOptions?.capacity ?? defaultCapacity) : { deliver: handled, drain: Effect.void }, deliver = (event) => Option.match(select(event), {
36
+ const context = replayableContext(yield* Effect.context()), id = yield* Ref.getAndUpdate(nextId, (value) => value + 1), handled = (selected) => handle(selected).pipe(Effect.provide(context)), lane = (subscribeOptions?.mode ?? "inline") === "background" ? yield* background(handled, subscribeOptions?.capacity ?? defaultCapacity) : { deliver: handled, drain: Effect.void }, deliver = (event) => Option.match(select(event), {
36
37
  onNone: () => Effect.void,
37
38
  onSome: lane.deliver
38
39
  });
@@ -9,18 +9,22 @@ export declare const CompletionVerdict: Schema.Struct<{
9
9
  readonly facts: Schema.$Record<Schema.String, Schema.Unknown>;
10
10
  }>;
11
11
  export type CompletionVerdict = typeof CompletionVerdict.Type;
12
- export declare const TurnStartedEvent: Schema.TaggedStruct<"turn.started", {
12
+ export declare const TurnStartedEvent: Schema.Struct<{
13
+ readonly _tag: Schema.tag<"turn.started">;
13
14
  readonly runId: Schema.String;
14
15
  readonly turn: Schema.Int;
15
16
  readonly userMessage: typeof UserMessage;
17
+ readonly meta: Schema.$Record<Schema.String, Schema.Unknown>;
18
+ readonly resumes: Schema.optionalKey<Schema.Int>;
16
19
  }>;
17
- export declare const StepStartedEvent: Schema.TaggedStruct<"step.started", {
20
+ export declare const StepStartedEvent: Schema.Struct<{
21
+ readonly _tag: Schema.tag<"step.started">;
18
22
  readonly step: Schema.Int;
19
- /** Step zero of a host-planned batch: no provider call. */
20
23
  readonly planned: Schema.Boolean;
21
24
  readonly activeTools: Schema.$Array<Schema.String>;
22
25
  }>;
23
- export declare const ToolStartedEvent: Schema.TaggedStruct<"tool.started", {
26
+ export declare const ToolStartedEvent: Schema.Struct<{
27
+ readonly _tag: Schema.tag<"tool.started">;
24
28
  readonly step: Schema.Int;
25
29
  readonly invocationId: Schema.String;
26
30
  readonly tool: Schema.String;
@@ -28,20 +32,45 @@ export declare const ToolStartedEvent: Schema.TaggedStruct<"tool.started", {
28
32
  readonly labels: Schema.$Record<Schema.String, Schema.String>;
29
33
  readonly stage: Schema.OptionFromNullOr<Schema.String>;
30
34
  }>;
31
- export declare const ToolCompletedEvent: Schema.TaggedStruct<"tool.completed", {
35
+ /** Why the tool registry refused a call before its handler ran. */
36
+ export declare const ToolRefusalReason: Schema.Literals<readonly ["inactive", "forbidden", "denied", "call-limit", "tool-call-limit"]>;
37
+ export type ToolRefusalReason = typeof ToolRefusalReason.Type;
38
+ export declare const ToolRefusedEvent: Schema.Struct<{
39
+ readonly _tag: Schema.tag<"tool.refused">;
40
+ readonly step: Schema.Int;
41
+ readonly invocationId: Schema.String;
42
+ readonly tool: Schema.String;
43
+ readonly input: Schema.Unknown;
44
+ readonly reason: Schema.Literals<readonly ["inactive", "forbidden", "denied", "call-limit", "tool-call-limit"]>;
45
+ readonly message: Schema.String;
46
+ }>;
47
+ export declare const ToolCompletedEvent: Schema.Struct<{
48
+ readonly _tag: Schema.tag<"tool.completed">;
32
49
  readonly step: Schema.Int;
33
50
  readonly invocationId: Schema.String;
34
51
  readonly tool: Schema.String;
35
52
  readonly input: Schema.Unknown;
36
53
  readonly ok: Schema.Boolean;
37
- /** The decoded value the handler returned (or failed with). Not journaled. */
38
54
  readonly result: Schema.Unknown;
39
- /** The wire form of `result`, as the model receives it. */
40
55
  readonly encoded: Schema.Unknown;
41
56
  readonly durationMs: Schema.Number;
42
57
  readonly labels: Schema.$Record<Schema.String, Schema.String>;
43
58
  readonly stage: Schema.OptionFromNullOr<Schema.String>;
44
59
  }>;
60
+ /**
61
+ * What a running tool reports before it completes: a command's output as it
62
+ * arrives (`stdout`, `stderr`), or a step of a long job (`status`).
63
+ * Transient: shown live, never journaled; `tool.completed` carries the
64
+ * result. A handler publishes it with `publishToolProgress`.
65
+ */
66
+ export declare const ToolProgressEvent: Schema.Struct<{
67
+ readonly _tag: Schema.tag<"tool.progress">;
68
+ readonly step: Schema.Int;
69
+ readonly invocationId: Schema.String;
70
+ readonly tool: Schema.String;
71
+ readonly channel: Schema.Literals<readonly ["stdout", "stderr", "status"]>;
72
+ readonly text: Schema.String;
73
+ }>;
45
74
  export declare const StepResult: Schema.Struct<{
46
75
  readonly entry: Schema.brand<Schema.Trimmed, "EntryId">;
47
76
  readonly toolCallId: Schema.brand<Schema.String, "ToolCallId">;
@@ -49,10 +78,10 @@ export declare const StepResult: Schema.Struct<{
49
78
  readonly ok: Schema.Boolean;
50
79
  }>;
51
80
  export type StepResult = typeof StepResult.Type;
52
- export declare const StepEndedEvent: Schema.TaggedStruct<"step.ended", {
81
+ export declare const StepEndedEvent: Schema.Struct<{
82
+ readonly _tag: Schema.tag<"step.ended">;
53
83
  readonly step: Schema.Int;
54
84
  readonly status: Schema.Literals<readonly ["completed", "failed", "cancelled"]>;
55
- /** The step's recorded tool results, in log order. */
56
85
  readonly results: Schema.$Array<Schema.Struct<{
57
86
  readonly entry: Schema.brand<Schema.Trimmed, "EntryId">;
58
87
  readonly toolCallId: Schema.brand<Schema.String, "ToolCallId">;
@@ -60,7 +89,8 @@ export declare const StepEndedEvent: Schema.TaggedStruct<"step.ended", {
60
89
  readonly ok: Schema.Boolean;
61
90
  }>>;
62
91
  }>;
63
- export declare const CompletionEvaluatedEvent: Schema.TaggedStruct<"completion.evaluated", {
92
+ export declare const CompletionEvaluatedEvent: Schema.Struct<{
93
+ readonly _tag: Schema.tag<"completion.evaluated">;
64
94
  readonly step: Schema.Int;
65
95
  readonly verdict: Schema.Struct<{
66
96
  readonly complete: Schema.Boolean;
@@ -70,12 +100,14 @@ export declare const CompletionEvaluatedEvent: Schema.TaggedStruct<"completion.e
70
100
  readonly facts: Schema.$Record<Schema.String, Schema.Unknown>;
71
101
  }>;
72
102
  }>;
73
- export declare const SkillsActivatedEvent: Schema.TaggedStruct<"skills.activated", {
103
+ export declare const SkillsActivatedEvent: Schema.Struct<{
104
+ readonly _tag: Schema.tag<"skills.activated">;
74
105
  readonly skills: Schema.$Array<Schema.String>;
75
106
  readonly tools: Schema.$Array<Schema.String>;
76
107
  readonly source: Schema.Literals<readonly ["always", "matcher", "load_skill", "host"]>;
77
108
  }>;
78
- export declare const ContextBuiltEvent: Schema.TaggedStruct<"context.built", {
109
+ export declare const ContextBuiltEvent: Schema.Struct<{
110
+ readonly _tag: Schema.tag<"context.built">;
79
111
  readonly step: Schema.Int;
80
112
  readonly turn: Schema.Int;
81
113
  readonly strategy: Schema.String;
@@ -84,10 +116,12 @@ export declare const ContextBuiltEvent: Schema.TaggedStruct<"context.built", {
84
116
  readonly systemFingerprint: Schema.String;
85
117
  readonly estimatedTokens: Schema.Int;
86
118
  readonly reservedTokens: Schema.Int;
119
+ readonly budgetTokens: Schema.Int;
87
120
  readonly compactions: Schema.Int;
88
121
  readonly activeTools: Schema.$Array<Schema.String>;
89
122
  }>;
90
- export declare const DecisionRecordedEvent: Schema.TaggedStruct<"decision.recorded", {
123
+ export declare const DecisionRecordedEvent: Schema.Struct<{
124
+ readonly _tag: Schema.tag<"decision.recorded">;
91
125
  readonly record: Schema.Struct<{
92
126
  readonly version: Schema.Literal<1>;
93
127
  readonly id: Schema.brand<Schema.Trimmed, "DecisionId">;
@@ -107,7 +141,8 @@ export declare const DecisionRecordedEvent: Schema.TaggedStruct<"decision.record
107
141
  readonly probabilities: Schema.OptionFromNullOr<Schema.$Record<Schema.String, Schema.Number>>;
108
142
  }>;
109
143
  }>;
110
- export declare const AssistantMessageEvent: Schema.TaggedStruct<"assistant.message", {
144
+ export declare const AssistantMessageEvent: Schema.Struct<{
145
+ readonly _tag: Schema.tag<"assistant.message">;
111
146
  readonly step: Schema.Int;
112
147
  readonly text: Schema.String;
113
148
  readonly reasoning: Schema.String;
@@ -122,66 +157,103 @@ export declare const AssistantMessageEvent: Schema.TaggedStruct<"assistant.messa
122
157
  readonly outputTokens: Schema.Number;
123
158
  readonly totalTokens: Schema.Number;
124
159
  readonly cacheReadTokens: Schema.Number;
160
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
161
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
125
162
  }>;
126
163
  }>;
127
164
  /** A streamed increment — transient: never journaled, restated by assistant.message. */
128
- export declare const AssistantDeltaEvent: Schema.TaggedStruct<"assistant.delta", {
165
+ export declare const AssistantDeltaEvent: Schema.Struct<{
166
+ readonly _tag: Schema.tag<"assistant.delta">;
129
167
  readonly step: Schema.Int;
130
168
  readonly channel: Schema.Literals<readonly ["text", "reasoning", "tool-params"]>;
131
169
  readonly id: Schema.String;
132
170
  readonly delta: Schema.String;
133
171
  }>;
134
- export declare const TurnEndedEvent: Schema.TaggedStruct<"turn.ended", {
172
+ export declare const StreamFallbackEvent: Schema.Struct<{
173
+ readonly _tag: Schema.tag<"stream.fallback">;
174
+ readonly step: Schema.Int;
175
+ readonly message: Schema.String;
176
+ }>;
177
+ export declare const TurnEndedEvent: Schema.Struct<{
178
+ readonly _tag: Schema.tag<"turn.ended">;
135
179
  readonly runId: Schema.String;
136
180
  readonly turn: Schema.Int;
137
181
  readonly outcome: Schema.Literals<readonly ["completed", "partial", "failed"]>;
138
182
  readonly reply: Schema.OptionFromNullOr<Schema.String>;
183
+ readonly usage: Schema.Struct<{
184
+ readonly inputTokens: Schema.Number;
185
+ readonly outputTokens: Schema.Number;
186
+ readonly totalTokens: Schema.Number;
187
+ readonly cacheReadTokens: Schema.Number;
188
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
189
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
190
+ }>;
139
191
  }>;
140
192
  /** An application's own event (see `defineHostEvent`); `name` is the host's namespace. */
141
- export declare const HostEvent: Schema.TaggedStruct<"host", {
193
+ export declare const HostEvent: Schema.Struct<{
194
+ readonly _tag: Schema.tag<"host">;
142
195
  readonly name: Schema.Trimmed;
143
196
  readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
144
197
  }>;
145
- export declare const TurnEvent: Schema.Union<readonly [Schema.TaggedStruct<"turn.started", {
198
+ export declare const TurnEvent: Schema.Union<readonly [Schema.Struct<{
199
+ readonly _tag: Schema.tag<"turn.started">;
146
200
  readonly runId: Schema.String;
147
201
  readonly turn: Schema.Int;
148
202
  readonly userMessage: typeof UserMessage;
149
- }>, Schema.TaggedStruct<"step.started", {
203
+ readonly meta: Schema.$Record<Schema.String, Schema.Unknown>;
204
+ readonly resumes: Schema.optionalKey<Schema.Int>;
205
+ }>, Schema.Struct<{
206
+ readonly _tag: Schema.tag<"step.started">;
150
207
  readonly step: Schema.Int;
151
- /** Step zero of a host-planned batch: no provider call. */
152
208
  readonly planned: Schema.Boolean;
153
209
  readonly activeTools: Schema.$Array<Schema.String>;
154
- }>, Schema.TaggedStruct<"tool.started", {
210
+ }>, Schema.Struct<{
211
+ readonly _tag: Schema.tag<"tool.started">;
155
212
  readonly step: Schema.Int;
156
213
  readonly invocationId: Schema.String;
157
214
  readonly tool: Schema.String;
158
215
  readonly input: Schema.Unknown;
159
216
  readonly labels: Schema.$Record<Schema.String, Schema.String>;
160
217
  readonly stage: Schema.OptionFromNullOr<Schema.String>;
161
- }>, Schema.TaggedStruct<"tool.completed", {
218
+ }>, Schema.Struct<{
219
+ readonly _tag: Schema.tag<"tool.refused">;
220
+ readonly step: Schema.Int;
221
+ readonly invocationId: Schema.String;
222
+ readonly tool: Schema.String;
223
+ readonly input: Schema.Unknown;
224
+ readonly reason: Schema.Literals<readonly ["inactive", "forbidden", "denied", "call-limit", "tool-call-limit"]>;
225
+ readonly message: Schema.String;
226
+ }>, Schema.Struct<{
227
+ readonly _tag: Schema.tag<"tool.progress">;
228
+ readonly step: Schema.Int;
229
+ readonly invocationId: Schema.String;
230
+ readonly tool: Schema.String;
231
+ readonly channel: Schema.Literals<readonly ["stdout", "stderr", "status"]>;
232
+ readonly text: Schema.String;
233
+ }>, Schema.Struct<{
234
+ readonly _tag: Schema.tag<"tool.completed">;
162
235
  readonly step: Schema.Int;
163
236
  readonly invocationId: Schema.String;
164
237
  readonly tool: Schema.String;
165
238
  readonly input: Schema.Unknown;
166
239
  readonly ok: Schema.Boolean;
167
- /** The decoded value the handler returned (or failed with). Not journaled. */
168
240
  readonly result: Schema.Unknown;
169
- /** The wire form of `result`, as the model receives it. */
170
241
  readonly encoded: Schema.Unknown;
171
242
  readonly durationMs: Schema.Number;
172
243
  readonly labels: Schema.$Record<Schema.String, Schema.String>;
173
244
  readonly stage: Schema.OptionFromNullOr<Schema.String>;
174
- }>, Schema.TaggedStruct<"step.ended", {
245
+ }>, Schema.Struct<{
246
+ readonly _tag: Schema.tag<"step.ended">;
175
247
  readonly step: Schema.Int;
176
248
  readonly status: Schema.Literals<readonly ["completed", "failed", "cancelled"]>;
177
- /** The step's recorded tool results, in log order. */
178
249
  readonly results: Schema.$Array<Schema.Struct<{
179
250
  readonly entry: Schema.brand<Schema.Trimmed, "EntryId">;
180
251
  readonly toolCallId: Schema.brand<Schema.String, "ToolCallId">;
181
252
  readonly tool: Schema.String;
182
253
  readonly ok: Schema.Boolean;
183
254
  }>>;
184
- }>, Schema.TaggedStruct<"completion.evaluated", {
255
+ }>, Schema.Struct<{
256
+ readonly _tag: Schema.tag<"completion.evaluated">;
185
257
  readonly step: Schema.Int;
186
258
  readonly verdict: Schema.Struct<{
187
259
  readonly complete: Schema.Boolean;
@@ -190,11 +262,13 @@ export declare const TurnEvent: Schema.Union<readonly [Schema.TaggedStruct<"turn
190
262
  /** Host facts behind the verdict, journaled with it. */
191
263
  readonly facts: Schema.$Record<Schema.String, Schema.Unknown>;
192
264
  }>;
193
- }>, Schema.TaggedStruct<"skills.activated", {
265
+ }>, Schema.Struct<{
266
+ readonly _tag: Schema.tag<"skills.activated">;
194
267
  readonly skills: Schema.$Array<Schema.String>;
195
268
  readonly tools: Schema.$Array<Schema.String>;
196
269
  readonly source: Schema.Literals<readonly ["always", "matcher", "load_skill", "host"]>;
197
- }>, Schema.TaggedStruct<"context.built", {
270
+ }>, Schema.Struct<{
271
+ readonly _tag: Schema.tag<"context.built">;
198
272
  readonly step: Schema.Int;
199
273
  readonly turn: Schema.Int;
200
274
  readonly strategy: Schema.String;
@@ -203,9 +277,11 @@ export declare const TurnEvent: Schema.Union<readonly [Schema.TaggedStruct<"turn
203
277
  readonly systemFingerprint: Schema.String;
204
278
  readonly estimatedTokens: Schema.Int;
205
279
  readonly reservedTokens: Schema.Int;
280
+ readonly budgetTokens: Schema.Int;
206
281
  readonly compactions: Schema.Int;
207
282
  readonly activeTools: Schema.$Array<Schema.String>;
208
- }>, Schema.TaggedStruct<"decision.recorded", {
283
+ }>, Schema.Struct<{
284
+ readonly _tag: Schema.tag<"decision.recorded">;
209
285
  readonly record: Schema.Struct<{
210
286
  readonly version: Schema.Literal<1>;
211
287
  readonly id: Schema.brand<Schema.Trimmed, "DecisionId">;
@@ -224,7 +300,8 @@ export declare const TurnEvent: Schema.Union<readonly [Schema.TaggedStruct<"turn
224
300
  readonly applied: Schema.OptionFromNullOr<Schema.String>;
225
301
  readonly probabilities: Schema.OptionFromNullOr<Schema.$Record<Schema.String, Schema.Number>>;
226
302
  }>;
227
- }>, Schema.TaggedStruct<"assistant.message", {
303
+ }>, Schema.Struct<{
304
+ readonly _tag: Schema.tag<"assistant.message">;
228
305
  readonly step: Schema.Int;
229
306
  readonly text: Schema.String;
230
307
  readonly reasoning: Schema.String;
@@ -239,18 +316,35 @@ export declare const TurnEvent: Schema.Union<readonly [Schema.TaggedStruct<"turn
239
316
  readonly outputTokens: Schema.Number;
240
317
  readonly totalTokens: Schema.Number;
241
318
  readonly cacheReadTokens: Schema.Number;
319
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
320
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
242
321
  }>;
243
- }>, Schema.TaggedStruct<"assistant.delta", {
322
+ }>, Schema.Struct<{
323
+ readonly _tag: Schema.tag<"assistant.delta">;
244
324
  readonly step: Schema.Int;
245
325
  readonly channel: Schema.Literals<readonly ["text", "reasoning", "tool-params"]>;
246
326
  readonly id: Schema.String;
247
327
  readonly delta: Schema.String;
248
- }>, Schema.TaggedStruct<"turn.ended", {
328
+ }>, Schema.Struct<{
329
+ readonly _tag: Schema.tag<"stream.fallback">;
330
+ readonly step: Schema.Int;
331
+ readonly message: Schema.String;
332
+ }>, Schema.Struct<{
333
+ readonly _tag: Schema.tag<"turn.ended">;
249
334
  readonly runId: Schema.String;
250
335
  readonly turn: Schema.Int;
251
336
  readonly outcome: Schema.Literals<readonly ["completed", "partial", "failed"]>;
252
337
  readonly reply: Schema.OptionFromNullOr<Schema.String>;
253
- }>, Schema.TaggedStruct<"host", {
338
+ readonly usage: Schema.Struct<{
339
+ readonly inputTokens: Schema.Number;
340
+ readonly outputTokens: Schema.Number;
341
+ readonly totalTokens: Schema.Number;
342
+ readonly cacheReadTokens: Schema.Number;
343
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
344
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
345
+ }>;
346
+ }>, Schema.Struct<{
347
+ readonly _tag: Schema.tag<"host">;
254
348
  readonly name: Schema.Trimmed;
255
349
  readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
256
350
  }>]>;
@@ -261,3 +355,9 @@ export type TurnEventOf<Name extends TurnEventName> = Extract<TurnEvent, {
261
355
  }>;
262
356
  /** Events that exist only for live rendering and are never journaled. */
263
357
  export declare const TransientTurnEvents: ReadonlyArray<TurnEventName>;
358
+ /**
359
+ * Every bus event's name, in the union's order. All of them are reserved
360
+ * (`RESERVED_KINDS`): a host event may not take one, whether the journal
361
+ * stores the bus event under that name or not.
362
+ */
363
+ export declare const TURN_EVENT_NAMES: ReadonlyArray<TurnEventName>;
@@ -33,3 +33,14 @@ export declare const defineHostEvent: <A, I extends Readonly<Record<string, unkn
33
33
  publish: (events: TurnEventsService, data: A) => Effect.Effect<void, HarnessError, never>;
34
34
  on: <R>(handle: (data: A) => Effect.Effect<void, HarnessError, R>, options?: SubscribeOptions) => Subscription<R>;
35
35
  };
36
+ /**
37
+ * Report progress from inside a tool handler: publishes a transient
38
+ * `tool.progress` with the call's step, invocation id and tool name (from
39
+ * `CurrentToolInvocation`) on the run's bus. `channel` is `stdout` or
40
+ * `stderr` for a command's output as it arrives, `status` for anything else.
41
+ * Outside a tool call, or without a `RunContext`, it does nothing. As for
42
+ * `tool.started`, a subscriber failure fails the turn: the handler stops
43
+ * there, and the tool registry fails the turn after `tool.completed`
44
+ * instead of handing the failure to the model as the tool's result.
45
+ */
46
+ export declare const publishToolProgress: (channel: TurnEventOf<"tool.progress">["channel"], text: string) => Effect.Effect<void>;
@@ -1,5 +1,7 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
2
  import { HarnessError } from "../harness/plugin.entity.js";
3
+ import { CurrentToolInvocation } from "../loop/stepContext.js";
4
+ import { RunContext } from "../ports/run-context.port.js";
3
5
  const isNamed = (name) => (event) => event._tag === name;
4
6
  export const onEvent = (name, handle, options) => (events) => events.subscribe((event) => Option.liftPredicate(event, isNamed(name)), handle, options), onTool = (tool, handle, options) => {
5
7
  const isInput = Schema.is(tool.parametersSchema), isResult = Schema.is(tool.successSchema);
@@ -18,4 +20,10 @@ export const onEvent = (name, handle, options) => (events) => events.subscribe((
18
20
  publish: (events, data) => make(data).pipe(Effect.flatMap(events.publish)),
19
21
  on: (handle, options) => (events) => events.subscribe(select, handle, options)
20
22
  };
21
- };
23
+ }, publishToolProgress = (channel, text) => Effect.gen(function* () {
24
+ const invocation = yield* Effect.service(CurrentToolInvocation), run = yield* Effect.serviceOption(RunContext);
25
+ if (Option.isNone(invocation) || Option.isNone(run))
26
+ return;
27
+ const { step, invocationId, tool } = invocation.value;
28
+ yield* run.value.events.publish({ _tag: "tool.progress", step, invocationId, tool, channel, text }).pipe(Effect.orDie);
29
+ });