@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
@@ -3,6 +3,7 @@ import { DecisionRecord } from "../decision-record.entity.js";
3
3
  import { ToolCallId } from "../domain/message.entity.js";
4
4
  import { TokenUsage } from "../domain/token-usage.entity.js";
5
5
  import { ActivationSource, EntryId } from "../memory/memory-log.entity.js";
6
+ import { JsonObject } from "../session/session-log.entity.js";
6
7
  import { UserMessage } from "./user-message.entity.js";
7
8
  const Labels = Schema.Record(Schema.String, Schema.String), Data = Schema.Record(Schema.String, Schema.Unknown);
8
9
  export const CompletionVerdict = Schema.Struct({
@@ -12,11 +13,17 @@ export const CompletionVerdict = Schema.Struct({
12
13
  }), TurnStartedEvent = Schema.TaggedStruct("turn.started", {
13
14
  runId: Schema.String,
14
15
  turn: Schema.Int,
15
- userMessage: UserMessage
16
+ userMessage: UserMessage,
17
+ meta: JsonObject,
18
+ resumes: Schema.optionalKey(Schema.Int)
19
+ }).annotate({
20
+ description: "Published once per turn when memory takes the user's message as turn `turn` (`TurnMemory.persistMessage`; `Agent.turn` runs it before the host's `use`). `runId` names the run, `userMessage` is what the user said (as the session stored it: its images, inline as base64, normalised), `meta` is the host's metadata the turn was begun with (`{}` when none was given; never shown to the model), and `resumes`, present only on a turn that resumes an earlier one, is that turn's number. Not journaled from the bus: the session stored its own `turn.started` when the turn began, with `meta` and `resumes` only when they were given. A subscription made in `use` never sees it; one in the host's per-turn layer or a session plugin does."
16
21
  }), StepStartedEvent = Schema.TaggedStruct("step.started", {
17
22
  step: Schema.Int,
18
23
  planned: Schema.Boolean,
19
24
  activeTools: Schema.Array(Schema.String)
25
+ }).annotate({
26
+ description: "Published by the step loop as each step starts. `planned` is true for step 0 of a host-planned batch, which makes no provider call; `activeTools` are the tool names the step offers the model. Journaled as `step.started`."
20
27
  }), ToolStartedEvent = Schema.TaggedStruct("tool.started", {
21
28
  step: Schema.Int,
22
29
  invocationId: Schema.String,
@@ -24,6 +31,17 @@ export const CompletionVerdict = Schema.Struct({
24
31
  input: Schema.Unknown,
25
32
  labels: Labels,
26
33
  stage: Schema.OptionFromNullOr(Schema.String)
34
+ }).annotate({
35
+ description: "Published by the tool registry just before a tool's handler runs. `invocationId` (`<runId>:call:<n>`) pairs it with its `tool.progress` and `tool.completed`; `input` is the decoded parameters; `labels` and `stage` come from the tool's annotations. A refused call (inactive, forbidden, denied, over a limit) publishes `tool.refused` instead, and no other `tool.*` event; `step.ended.results` lists every recorded result, refused calls included. Journaled as `tool.started`, input included."
36
+ }), ToolRefusalReason = Schema.Literals(["inactive", "forbidden", "denied", "call-limit", "tool-call-limit"]), ToolRefusedEvent = Schema.TaggedStruct("tool.refused", {
37
+ step: Schema.Int,
38
+ invocationId: Schema.String,
39
+ tool: Schema.String,
40
+ input: Schema.Unknown,
41
+ reason: ToolRefusalReason,
42
+ message: Schema.String
43
+ }).annotate({
44
+ description: "Published by the tool registry when it refuses a call before the handler runs, in place of `tool.started`: the tool is not active (`inactive`: not loaded, or not available in this conversation), the turn lacks a permission it needs (`forbidden`), the host's `ActionPolicy` denied it (`denied`), the turn's call limit is reached (`call-limit`) or the tool's own (`tool-call-limit`). `invocationId` is minted for the call as for any other (the refused call takes its `<runId>:call:<n>`); `message` is what the model receives as the call's failed result, which `step.ended.results` lists as not ok. The model is offered only active tools, so `inactive` comes from a call made through the handlers directly (a host's, another tool's); `forbidden` also from a tool an earlier turn activated whose permission this turn lacks. Journaled as `tool.refused` without `input`."
27
45
  }), ToolCompletedEvent = Schema.TaggedStruct("tool.completed", {
28
46
  step: Schema.Int,
29
47
  invocationId: Schema.String,
@@ -35,6 +53,16 @@ export const CompletionVerdict = Schema.Struct({
35
53
  durationMs: Schema.Number,
36
54
  labels: Labels,
37
55
  stage: Schema.OptionFromNullOr(Schema.String)
56
+ }).annotate({
57
+ description: "Published by the tool registry when a handler returned or failed (`ok`). `result` is the decoded value, `encoded` its wire form as the model receives it. `durationMs` runs from just after `tool.started` to the handler's exit, so it includes any wait for the tool's read or write lane. Journaled as `tool.completed` without `input`, `result` and `encoded`: memory keeps the call and its result (`memory.message`, `memory.tool-result`)."
58
+ }), ToolProgressEvent = Schema.TaggedStruct("tool.progress", {
59
+ step: Schema.Int,
60
+ invocationId: Schema.String,
61
+ tool: Schema.String,
62
+ channel: Schema.Literals(["stdout", "stderr", "status"]),
63
+ text: Schema.String
64
+ }).annotate({
65
+ description: "Published by `publishToolProgress` from inside a tool handler, between that call's `tool.started` and `tool.completed` (same `step`, `invocationId` and `tool`). `channel` is stdout or stderr for a command's output as it arrives, status for anything else the tool reports; `text` is that output or message. Transient: never journaled; `tool.completed` carries the result."
38
66
  }), StepResult = Schema.Struct({
39
67
  entry: EntryId,
40
68
  toolCallId: ToolCallId,
@@ -44,13 +72,19 @@ export const CompletionVerdict = Schema.Struct({
44
72
  step: Schema.Int,
45
73
  status: Schema.Literals(["completed", "failed", "cancelled"]),
46
74
  results: Schema.Array(StepResult)
75
+ }).annotate({
76
+ description: "Published by the step loop after the step's tail is recorded. `status` is completed, failed or cancelled; `results` are the step's recorded tool results (memory entry, tool call id, tool, ok), in log order, refused calls included. Journaled as `step.ended`."
47
77
  }), CompletionEvaluatedEvent = Schema.TaggedStruct("completion.evaluated", {
48
78
  step: Schema.Int,
49
79
  verdict: CompletionVerdict
80
+ }).annotate({
81
+ description: "Published by the step loop after `step.ended`, with the verdict of the host's completion policy for that step. A verdict that awaits tasks is evaluated again once they finish, publishing a second event. Journaled as `completion.evaluated`."
50
82
  }), SkillsActivatedEvent = Schema.TaggedStruct("skills.activated", {
51
83
  skills: Schema.Array(Schema.String),
52
84
  tools: Schema.Array(Schema.String),
53
85
  source: ActivationSource
86
+ }).annotate({
87
+ description: "Published by the tool registry when skills (and their tools) become active: always-on skills, the intent matcher, the model's `load_skill`, or the host. Not journaled from the bus: memory records the activation as `memory.skills`."
54
88
  }), ContextBuiltEvent = Schema.TaggedStruct("context.built", {
55
89
  step: Schema.Int,
56
90
  turn: Schema.Int,
@@ -60,10 +94,15 @@ export const CompletionVerdict = Schema.Struct({
60
94
  systemFingerprint: Schema.String,
61
95
  estimatedTokens: Schema.Int,
62
96
  reservedTokens: Schema.Int,
97
+ budgetTokens: Schema.Int,
63
98
  compactions: Schema.Int,
64
99
  activeTools: Schema.Array(Schema.String)
100
+ }).annotate({
101
+ description: "Published before each provider call, once the step's context is built (none for a planned step 0). It carries the memory strategy and its version, the context and system-prompt fingerprints, the token estimate of the messages, the tokens reserved for the system prompt and tool schemas, the run's `budgetTokens` (the whole request's; memory compacted the messages toward `budgetTokens - reservedTokens`), how many compactions the build applied, and the active tools. Journaled as `context.built` (records written before 0.14.0 have no `budgetTokens`); the full request is journaled beside it as `request.prepared`."
65
102
  }), DecisionRecordedEvent = Schema.TaggedStruct("decision.recorded", {
66
103
  record: DecisionRecord
104
+ }).annotate({
105
+ description: "Published by `recordDecision`, for example when `turn.tools.select` or `apply` records the skill selection: the decision record holds the candidates, the policy version and what was chosen. Journaled as `decision.recorded`."
67
106
  }), AssistantMessageEvent = Schema.TaggedStruct("assistant.message", {
68
107
  step: Schema.Int,
69
108
  text: Schema.String,
@@ -71,23 +110,39 @@ export const CompletionVerdict = Schema.Struct({
71
110
  model: Schema.OptionFromNullOr(Schema.String),
72
111
  toolCalls: Schema.Array(Schema.Struct({ id: Schema.String, tool: Schema.String, input: Schema.Unknown })),
73
112
  usage: TokenUsage
113
+ }).annotate({
114
+ description: "Published by the step loop once per provider response, after that step's tools ran and its tail is recorded: the text, the reasoning, the model, the tool calls and the token usage (input with its cache reads and writes inside, output with its reasoning inside). A host-planned step 0 carries zero usage. Journaled only as `step.usage` (step, model, usage): the content itself is memory's `memory.message`."
74
115
  }), AssistantDeltaEvent = Schema.TaggedStruct("assistant.delta", {
75
116
  step: Schema.Int,
76
117
  channel: Schema.Literals(["text", "reasoning", "tool-params"]),
77
118
  id: Schema.String,
78
119
  delta: Schema.String
120
+ }).annotate({
121
+ description: "Published by the step loop for each streamed increment while `limits.streaming` is on (the default), on channel text, reasoning or tool-params; `id` groups the increments of one content part. Transient: never journaled, and restated in full by `assistant.message`. A stream that falls back to a non-streamed call (`stream.fallback`) publishes none."
122
+ }), StreamFallbackEvent = Schema.TaggedStruct("stream.fallback", {
123
+ step: Schema.Int,
124
+ message: Schema.String
125
+ }).annotate({
126
+ description: "Published by the step loop when a streamed provider call fails before its first part: the step is answered by a non-streamed call instead, and so is every later step of the run. Once per run at most. `message` is the stream's error as text, cut to 300 characters; it can quote the provider's error. A stream that fails after its first part fails the step instead (its tools may have run). A refusal over an image (an `InvalidRequestError` that names one) is no stream failure: it has no fallback; the turn omits the images and sends the step again (`StepRequest.onMediaRejected`), or the step fails. Journaled as `stream.fallback`."
79
127
  }), TurnEndedEvent = Schema.TaggedStruct("turn.ended", {
80
128
  runId: Schema.String,
81
129
  turn: Schema.Int,
82
130
  outcome: Schema.Literals(["completed", "partial", "failed"]),
83
- reply: Schema.OptionFromNullOr(Schema.String)
131
+ reply: Schema.OptionFromNullOr(Schema.String),
132
+ usage: TokenUsage
133
+ }).annotate({
134
+ description: "Published once when the turn's reply is recorded (`TurnMemory.persistReply`, from `guardTurn`), also when the turn fails, with outcome failed; `usage` is the turn's usage, the usage of its `assistant.message` events summed (`RunContext.usage`). Under `Agent.turn` it is published after `use`, the host's per-turn layer and the turn-dependent plugins have closed, and their subscriptions with them: only a subscription whose scope is still open sees it, such as one made with `Scope.provide(observerScope)` over the caller's outer scope, or one registered around `TurnLive` by a host composing the turn itself. Otherwise read the `TurnResult` `Agent.turn` returns (its `usage` is the same total), or the stored `turn.reply` and `turn.ended`, and `turnUsageOf` over the stored events. Not journaled from the bus: memory stores `turn.reply` and the session its own `turn.ended` (reason and failure, no usage)."
84
135
  }), HostEvent = Schema.TaggedStruct("host", {
85
136
  name: Schema.Trimmed.check(Schema.isNonEmpty()),
86
137
  data: Data
138
+ }).annotate({
139
+ description: "An application's own event (`defineHostEvent`), published by the host's code, tools or tasks. `name` is the host's namespace and `data` the payload as its schema encodes it. Journaled under `name` as its kind; a name in `RESERVED_KINDS` fails the publish with `events.reserved`."
87
140
  }), TurnEvent = Schema.Union([
88
141
  TurnStartedEvent,
89
142
  StepStartedEvent,
90
143
  ToolStartedEvent,
144
+ ToolRefusedEvent,
145
+ ToolProgressEvent,
91
146
  ToolCompletedEvent,
92
147
  StepEndedEvent,
93
148
  CompletionEvaluatedEvent,
@@ -96,6 +151,24 @@ export const CompletionVerdict = Schema.Struct({
96
151
  DecisionRecordedEvent,
97
152
  AssistantMessageEvent,
98
153
  AssistantDeltaEvent,
154
+ StreamFallbackEvent,
99
155
  TurnEndedEvent,
100
156
  HostEvent
101
- ]), TransientTurnEvents = ["assistant.delta"];
157
+ ]), TransientTurnEvents = ["assistant.delta", "tool.progress"], TURN_EVENT_NAMES = [
158
+ "turn.started",
159
+ "step.started",
160
+ "tool.started",
161
+ "tool.refused",
162
+ "tool.progress",
163
+ "tool.completed",
164
+ "step.ended",
165
+ "completion.evaluated",
166
+ "skills.activated",
167
+ "context.built",
168
+ "decision.recorded",
169
+ "assistant.message",
170
+ "assistant.delta",
171
+ "stream.fallback",
172
+ "turn.ended",
173
+ "host"
174
+ ];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,13 @@
1
1
  import { Effect } from "effect";
2
2
  import type { HarnessError } from "../harness/plugin.entity.js";
3
3
  import { RunContext } from "../ports/run-context.port.js";
4
+ import type { TurnWriter } from "../ports/sessions.port.js";
4
5
  import type { StepLoop } from "../ports/step-loop.port.js";
5
6
  import type { RunTools } from "../ports/tool-registry.port.js";
6
7
  import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
7
8
  import { TurnMemory, TurnToolbox } from "../ports/turn-scope.port.js";
8
9
  import type { TurnRunOptions } from "../ports/turn-scope.port.js";
9
- import type { Turn, TurnOutcome, TurnTools } from "../ports/turn.port.js";
10
+ import type { Turn, TurnOutcome, TurnResult, TurnTools } from "../ports/turn.port.js";
10
11
  import type { TurnRunServices } from "./turn-run.js";
11
12
  /** Open the turn's tools (once). Their handlers run with the services of where this runs: open them inside the host's layer. */
12
13
  export declare const openTurnTools: Effect.Effect<RunTools, HarnessError, TurnToolbox>;
@@ -18,15 +19,52 @@ export declare const settleTurn: Effect.Effect<void, HarnessError, TurnEvents |
18
19
  export declare const finishTurn: (outcome: TurnOutcome) => Effect.Effect<void, HarnessError, TurnMemory | TurnEvents | RunContext>;
19
20
  /**
20
21
  * Run a turn's body to its end: once it succeeds, the turn settles (tasks
21
- * and background reactions) and finishes with its outcome. When it fails
22
- * or is interrupted, the turn finishes as failed and keeps the body's
23
- * cause. A HarnessError carried as a defect (a subscriber's failure behind
24
- * a `never` error channel) fails the turn, typed.
22
+ * and background reactions) and finishes with its outcome, returned with
23
+ * the turn's usage (`RunContext.usage`, the same total `turn.ended`
24
+ * carries). When it fails or is interrupted, the turn finishes as failed
25
+ * and keeps the body's cause, and the usage its provider calls spent so far
26
+ * goes on the current span as `efferent.usage.*_tokens` (the turn's
27
+ * `agent.turn` when `guardTurn` runs under `traceTurn` with no span between,
28
+ * as in `Agent.turn`; `traceTurn` records it itself when the turn returns).
29
+ * A HarnessError carried as a defect (a subscriber's failure behind a
30
+ * `never` error channel) fails the turn, typed.
25
31
  */
26
- export declare const guardTurn: <E, R>(body: Effect.Effect<TurnOutcome, E, R>) => Effect.Effect<TurnOutcome, E | HarnessError, R | TurnMemory | TurnEvents | TurnTasks | RunContext>;
32
+ export declare const guardTurn: <E, R>(body: Effect.Effect<TurnOutcome, E, R>) => Effect.Effect<TurnResult, E | HarnessError, R | TurnMemory | TurnEvents | TurnTasks | RunContext>;
27
33
  /**
28
34
  * The `Turn` a host's code works with, over the turn's services (the one
29
- * `Agent.turn` hands to `use`): its `run` is `runTurnLoop` with `options`.
30
- * The message must be persisted and the tools open.
35
+ * `Agent.turn` hands to `use`): its `run` is `runTurnLoop` with `options`,
36
+ * its spans under the span current where `run` is called. The message must
37
+ * be persisted and the tools open.
31
38
  */
32
39
  export declare const turnOf: (options?: TurnRunOptions) => Effect.Effect<Turn, HarnessError, TurnRunServices | RunContext | StepLoop>;
40
+ /** What a turn's span (`traceTurn`) shows besides the turn's ids. */
41
+ export interface TurnTraceOptions {
42
+ /**
43
+ * The keys of the turn's metadata (`AdmittedTurn.meta`) whose values the
44
+ * span carries, as `efferent.meta.<key>`: none by default, since metadata
45
+ * may hold what a trace backend should not (a user, a tenant). A string,
46
+ * number or boolean is kept as it is, anything else as its JSON; a key the
47
+ * turn's metadata lacks is left out.
48
+ */
49
+ readonly spanMeta?: ReadonlyArray<string>;
50
+ }
51
+ /**
52
+ * Run a turn under its span, `agent.turn`, the root of everything the turn
53
+ * does (under the caller's span, if any). It starts once the turn is
54
+ * admitted (`writer`), with `efferent.session.id`, `efferent.run.id`,
55
+ * `efferent.turn`, `gen_ai.conversation.id` (the session), `efferent.origin`,
56
+ * `efferent.resumes` (on a resume) and `options.spanMeta`'s
57
+ * `efferent.meta.<key>`; every span under it
58
+ * carries the first three too. It ends with `efferent.outcome` (the
59
+ * `TurnEnding` reason of the turn's exit: completed, partial, failed or
60
+ * interrupted; a turn cancelled or reaped while it ran shows as failed with
61
+ * `turn.closed`), `efferent.failure.code` when the turn failed or was
62
+ * interrupted, and the turn's usage
63
+ * (`efferent.usage.{input,output,cache_read,cache_write,reasoning,total}_tokens`):
64
+ * when it returned, from its result; when it failed, what `guardTurn`
65
+ * records, so `guardTurn` runs under this span with no span between.
66
+ * `Agent.turn` encloses everything after `Sessions.begin`
67
+ * in it: the turn-dependent plugins, the host's layer, the tools opened, `use`,
68
+ * the reply and the end; a host composing its own turn wraps the same.
69
+ */
70
+ export declare const traceTurn: (writer: TurnWriter, options?: TurnTraceOptions) => <E, R>(effect: Effect.Effect<TurnResult, E, R>) => Effect.Effect<TurnResult, E, R>;
@@ -1,7 +1,9 @@
1
1
  import { Effect, Exit, Option } from "effect";
2
+ import { replayableContext } from "../loop/traceContent.js";
2
3
  import { RunContext } from "../ports/run-context.port.js";
3
4
  import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
4
5
  import { TurnMemory, TurnToolbox } from "../ports/turn-scope.port.js";
6
+ import { turnEndingOf } from "../session/sessions.entity.functions.js";
5
7
  import { harnessDefectsAsFailures } from "./turn-bus.js";
6
8
  import { runTurnLoop } from "./turn-run.js";
7
9
  const failed = { outcome: "failed", reply: Option.none() };
@@ -27,14 +29,15 @@ export const openTurnTools = TurnToolbox.pipe(Effect.flatMap((toolbox) => toolbo
27
29
  yield* (yield* RunContext).flush;
28
30
  }), guardTurn = (body) => harnessDefectsAsFailures(body.pipe(Effect.tap(() => settleTurn))).pipe(Effect.onExit(Exit.match({
29
31
  onSuccess: () => Effect.void,
30
- onFailure: () => finishTurn(failed).pipe(Effect.catchCause(() => Effect.void))
31
- })), Effect.tap(finishTurn)), turnOf = (options = {}) => Effect.gen(function* () {
32
- const services = yield* Effect.context(), run = yield* RunContext, memory = yield* TurnMemory;
32
+ onFailure: () => finishTurn(failed).pipe(Effect.catchCause(() => Effect.void), Effect.andThen(RunContext.pipe(Effect.flatMap((run) => run.usage), Effect.flatMap((usage) => Effect.annotateCurrentSpan(turnUsageAttributes(usage))))))
33
+ })), Effect.tap(finishTurn), Effect.flatMap((outcome) => RunContext.pipe(Effect.flatMap((run) => run.usage), Effect.map((usage) => ({ outcome: outcome.outcome, reply: outcome.reply, usage }))))), turnOf = (options = {}) => Effect.gen(function* () {
34
+ const services = replayableContext(yield* Effect.context()), run = yield* RunContext, memory = yield* TurnMemory;
33
35
  return {
34
36
  conversation: run.conversation,
35
37
  runId: run.runId,
36
38
  turn: yield* memory.number,
37
39
  userMessage: run.userMessage,
40
+ admitted: run.admitted,
38
41
  memory: run.memory,
39
42
  events: run.events,
40
43
  tasks: run.tasks,
@@ -42,7 +45,34 @@ export const openTurnTools = TurnToolbox.pipe(Effect.flatMap((toolbox) => toolbo
42
45
  context: memory.context,
43
46
  reply: (text) => Effect.succeed({ outcome: "completed", reply: Option.some(text) }),
44
47
  run: (policy) => runTurnLoop(policy, options).pipe(Effect.provide(services)),
48
+ usage: run.usage,
45
49
  flush: run.flush,
46
50
  write: run.write
47
51
  };
48
52
  });
53
+ const turnIds = (admitted) => ({
54
+ "efferent.session.id": admitted.session.id,
55
+ "efferent.run.id": admitted.runId,
56
+ "efferent.turn": admitted.turn
57
+ }), metaAttribute = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value), turnSpanAttributes = (admitted, spanMeta) => ({
58
+ ...turnIds(admitted),
59
+ "gen_ai.conversation.id": admitted.session.id,
60
+ "efferent.origin": admitted.origin,
61
+ ...admitted.resumes === void 0 ? {} : { "efferent.resumes": admitted.resumes },
62
+ ...Object.fromEntries(spanMeta.filter((key) => Object.hasOwn(admitted.meta, key)).map((key) => [`efferent.meta.${key}`, metaAttribute(admitted.meta[key])]))
63
+ }), turnUsageAttributes = (usage) => ({
64
+ "efferent.usage.input_tokens": usage.inputTokens,
65
+ "efferent.usage.output_tokens": usage.outputTokens,
66
+ "efferent.usage.cache_read_tokens": usage.cacheReadTokens,
67
+ "efferent.usage.cache_write_tokens": usage.cacheWriteTokens,
68
+ "efferent.usage.reasoning_tokens": usage.reasoningTokens,
69
+ "efferent.usage.total_tokens": usage.totalTokens
70
+ }), turnEndAttributes = (exit) => {
71
+ const ending = turnEndingOf(exit);
72
+ return {
73
+ "efferent.outcome": ending.reason,
74
+ ...Option.match(ending.failure, { onNone: () => ({}), onSome: (failure) => ({ "efferent.failure.code": failure.code }) }),
75
+ ...Exit.match(exit, { onSuccess: (result) => turnUsageAttributes(result.usage), onFailure: () => ({}) })
76
+ };
77
+ };
78
+ export const traceTurn = (writer, options = {}) => (effect) => effect.pipe(Effect.onExit((exit) => Effect.annotateCurrentSpan(turnEndAttributes(exit))), Effect.annotateSpans(turnIds(writer.admitted)), Effect.withSpan("agent.turn", { attributes: turnSpanAttributes(writer.admitted, options.spanMeta ?? []) }));
@@ -0,0 +1 @@
1
+ export {};
@@ -15,11 +15,23 @@ export declare const schemaTokens: (tools: RunTools, active: ReadonlyArray<strin
15
15
  export declare const cacheKeyOf: (prefix: string, conversation: string) => Option.Option<string>;
16
16
  /**
17
17
  * Prepare one run of the step loop over the turn's services, in this
18
- * order: the policy's planned skills are activated, the turn-tier sections
19
- * are recorded, and memory is maintained for the turn's start. Everything
18
+ * order: the run's budget is checked (`turn.budget` unless it is an integer
19
+ * of at least 1), the policy's planned skills are activated, the turn-tier
20
+ * sections are recorded, and memory is maintained for the turn's start
21
+ * with the whole budget. Each step gives memory what the system prompt and
22
+ * the active tools' schemas leave of the budget, and fails with
23
+ * `context.budget` when they leave nothing. Everything
20
24
  * where this runs (the policy's services, the turn's, a host layer's) is
21
25
  * captured: the policy callbacks, the prompt sections and the tool handlers
22
- * run with it.
26
+ * run with it, without the span current here (`replayableContext`), so
27
+ * their spans nest under the step that calls them. Trace content is the
28
+ * policy's `captureTraceContent`, else the options', else
29
+ * `CurrentTraceContent` here. When a provider refuses a step's request over
30
+ * an image, `onMediaRejected` records an `OmitMedia` of the images that
31
+ * build sent, and the step's next plan, that one only, rebuilds the context
32
+ * with the same model choice, system prompt, directive and resolved model
33
+ * (no second `policy.model` or `policy.step` call, no second
34
+ * `StepContext`). Any other plan of a step calls the policy again.
23
35
  */
24
36
  export declare const stepRequestOf: <P>(policy: TurnPolicy<P>, options?: TurnRunOptions) => Effect.Effect<StepRequest, HarnessError, P | TurnRunServices>;
25
37
  /** One run of the turn: `stepRequestOf`, then the StepLoop. */
@@ -1,12 +1,14 @@
1
- import { Context, Effect, Option } from "effect";
1
+ import { Context, Effect, Option, Ref } from "effect";
2
2
  import { LanguageModel } from "effect/ai";
3
3
  import { toolParametersSchema } from "../loop/toolSchema.js";
4
+ import { isBudgetTokens } from "../domain/token-usage.entity.functions.js";
4
5
  import { HarnessError } from "../harness/plugin.entity.js";
5
- import { canonicalJson, estimateTokens, fingerprintOf } from "../memory/memory-log.entity.functions.js";
6
+ import { canonicalJson, entriesSendingMedia, estimateTokens, fingerprintOf } from "../memory/memory-log.entity.functions.js";
6
7
  import { StepLoop } from "../ports/step-loop.port.js";
7
8
  import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
8
9
  import { TurnMemory, TurnPrompt, TurnToolbox } from "../ports/turn-scope.port.js";
9
10
  import { CurrentModelCallPolicy } from "../loop/modelPolicy.js";
11
+ import { CurrentTraceContent, replayableContext } from "../loop/traceContent.js";
10
12
  import {
11
13
  checkModelRequest,
12
14
  modelRequestDescriptorOf,
@@ -15,26 +17,33 @@ import {
15
17
  reconstructModelRequest,
16
18
  resolveModelRequest
17
19
  } from "./model-request.entity.functions.js";
18
- const defaultLimits = { maxSteps: 50, toolConcurrency: 1, streaming: !0, requireCompletion: !1 }, defaultBudgetTokens = 64000, incomplete = { complete: !1, awaiting: [], facts: {} }, noDirective = { context: Option.none(), toolChoice: Option.none() };
20
+ const MEDIA_REJECTION_CHARS = 500, defaultLimits = { maxSteps: 50, toolConcurrency: 1, streaming: !0, requireCompletion: !1 }, defaultBudgetTokens = 64000, incomplete = { complete: !1, awaiting: [], facts: {} }, noDirective = { context: Option.none(), toolChoice: Option.none() }, failure = (code, message) => new HarnessError({ code, message });
19
21
  export const schemaTokens = (tools, active) => active.reduce((sum, name) => {
20
22
  const tool = tools.toolkit.tools[name];
21
23
  return tool === void 0 ? sum : sum + estimateTokens(`${tool.description ?? ""}${canonicalJson(toolParametersSchema(tool))}`);
22
24
  }, 0), cacheKeyOf = (prefix, conversation) => Option.map(Option.filter(Option.some(prefix), (value) => value.length > 0), (value) => `${value}:${conversation}`), stepRequestOf = (policy, options = {}) => Effect.gen(function* () {
23
- const runServices = yield* Effect.context(), inRun = (effect) => effect.pipe(Effect.provide(runServices)), memory = yield* TurnMemory, prompt = yield* TurnPrompt, events = yield* TurnEvents, tasks = yield* TurnTasks, tools = yield* (yield* TurnToolbox).tools, session = memory.session, runId = (yield* session.renderRecipe("none")).currentRun, defaultModel = Context.getOption(runServices, LanguageModel.LanguageModel), number = yield* memory.number, limits = { ...defaultLimits, ...options.limits, ...policy.limits }, budget = policy.budgetTokens ?? options.budgetTokens ?? defaultBudgetTokens, stepContext = policy.stepContext ?? "tail";
25
+ const runServices = replayableContext(yield* Effect.context()), inRun = (effect) => effect.pipe(Effect.provide(runServices)), memory = yield* TurnMemory, prompt = yield* TurnPrompt, events = yield* TurnEvents, tasks = yield* TurnTasks, tools = yield* (yield* TurnToolbox).tools, session = memory.session, runId = (yield* session.renderRecipe("none")).currentRun, defaultModel = Context.getOption(runServices, LanguageModel.LanguageModel), number = yield* memory.number, limits = { ...defaultLimits, ...options.limits, ...policy.limits }, budget = policy.budgetTokens ?? options.budgetTokens ?? defaultBudgetTokens;
26
+ if (!isBudgetTokens(budget))
27
+ return yield* Effect.fail(failure("turn.budget", `budgetTokens must be an integer of at least 1, not ${budget}`));
28
+ const stepContext = policy.stepContext ?? "tail";
24
29
  yield* Option.match(Option.fromNullishOr(policy.initial), {
25
30
  onNone: () => Effect.void,
26
31
  onSome: (batch) => batch.skills.length === 0 ? Effect.void : tools.activate(batch.skills, "host").pipe(Effect.asVoid)
27
32
  });
28
33
  yield* inRun(prompt.turnSections);
29
34
  yield* session.maintain({ phase: "turn-start", lastUsage: Option.none(), budgetTokens: budget, views: tools.views });
30
- const plan = (info) => Effect.gen(function* () {
35
+ const planned = yield* Ref.make(Option.none()), replanning = yield* Ref.make(Option.none()), decide = (info) => Effect.gen(function* () {
31
36
  const choice = policy.model === void 0 ? Option.none() : yield* inRun(policy.model(info)), system = yield* inRun(prompt.system(Option.flatMap(choice, (value) => value.variant))), directive = policy.step === void 0 ? noDirective : yield* inRun(policy.step(info));
32
37
  yield* Option.match(directive.context, {
33
38
  onNone: () => Effect.void,
34
39
  onSome: (text) => session.record([{ _tag: "StepContext", step: info.stepIndex, text }], info.stepIndex).pipe(Effect.asVoid)
35
40
  });
36
- const reserved = estimateTokens(system) + schemaTokens(tools, info.activeTools);
37
- yield* session.maintain({ phase: "step", lastUsage: info.lastUsage, budgetTokens: Math.max(1, budget - reserved), views: tools.views });
41
+ return { choice, system, directive };
42
+ }), plan = (info) => Effect.gen(function* () {
43
+ const again = Option.filter(yield* Ref.getAndSet(replanning, Option.none()), (last) => last.step === info.stepIndex), { choice, system, directive } = Option.isSome(again) ? again.value.decision : yield* decide(info), reserved = estimateTokens(system) + schemaTokens(tools, info.activeTools);
44
+ if (budget <= reserved)
45
+ return yield* Effect.fail(failure("context.budget", `The system prompt and tool schemas take ${reserved} tokens, all of the ${budget}-token context budget: raise budgetTokens or offer fewer tools`));
46
+ yield* session.maintain({ phase: "step", lastUsage: info.lastUsage, budgetTokens: budget - reserved, views: tools.views });
38
47
  const built = yield* session.build({ stepContext: stepContext === "tail" ? "tail" : "none" }), render = yield* session.renderRecipe(stepContext === "tail" ? "tail" : "none");
39
48
  yield* events.publish({
40
49
  _tag: "context.built",
@@ -46,15 +55,17 @@ export const schemaTokens = (tools, active) => active.reduce((sum, name) => {
46
55
  systemFingerprint: fingerprintOf(system),
47
56
  estimatedTokens: built.estimatedTokens,
48
57
  reservedTokens: reserved,
58
+ budgetTokens: budget,
49
59
  compactions: built.compactions.length,
50
60
  activeTools: info.activeTools
51
61
  });
52
62
  const stepText = stepContext === "system" ? Option.getOrElse(directive.context, () => "") : "", finalSystem = [system, stepText].filter((part) => part.length > 0).join(`
53
63
 
54
- `), model = yield* Option.match(Option.orElse(Option.map(choice, (value) => value.model), () => defaultModel), {
64
+ `), model = Option.isSome(again) ? again.value.model : yield* Option.match(Option.orElse(Option.map(choice, (value) => value.model), () => defaultModel), {
55
65
  onNone: () => Effect.succeed(Option.none()),
56
66
  onSome: (chosen) => resolveModelRequest(chosen).pipe(Effect.map(Option.some))
57
67
  });
68
+ yield* Ref.set(planned, Option.some({ step: info.stepIndex, decision: { choice, system, directive }, model, through: built.through, render }));
58
69
  yield* memory.prepareRequest({
59
70
  version: 1,
60
71
  runId,
@@ -95,11 +106,28 @@ export const schemaTokens = (tools, active) => active.reduce((sum, name) => {
95
106
  });
96
107
  }),
97
108
  record: (step, tail) => session.recordTail(tail, tools.views, step),
109
+ onMediaRejected: (reason) => Effect.gen(function* () {
110
+ const last = yield* Ref.get(planned);
111
+ if (Option.isNone(last))
112
+ return !1;
113
+ const entries = yield* session.entries, through = last.value.through, built = Option.isNone(through) ? [] : entries.slice(0, entries.findIndex((entry) => entry.id === through.value) + 1), sent = entriesSendingMedia(built, last.value.render);
114
+ if (sent.length === 0)
115
+ return !1;
116
+ yield* session.record([{
117
+ _tag: "Compaction",
118
+ strategy: session.strategy.id,
119
+ version: session.strategy.version,
120
+ action: { _tag: "OmitMedia", entries: sent, reason: reason.slice(0, MEDIA_REJECTION_CHARS) }
121
+ }], last.value.step);
122
+ yield* Ref.set(replanning, last);
123
+ return !0;
124
+ }),
98
125
  completion: (info) => policy.completion === void 0 ? Effect.succeed(incomplete) : inRun(policy.completion(info)),
99
126
  steering: options.steering ?? Effect.succeed(Option.none()),
100
127
  correctives: Option.fromNullishOr(policy.correctives),
101
128
  events,
102
129
  tasks,
103
- cacheKey: options.cacheKey ?? Option.none()
130
+ cacheKey: options.cacheKey ?? Option.none(),
131
+ captureTraceContent: policy.captureTraceContent ?? options.captureTraceContent ?? (yield* Effect.service(CurrentTraceContent))
104
132
  };
105
133
  }), runTurnLoop = (policy, options = {}) => stepRequestOf(policy, options).pipe(Effect.flatMap((request) => StepLoop.pipe(Effect.flatMap((loop) => loop.run(request)))));
@@ -9,11 +9,12 @@ import type { TurnLiveInput } from "../ports/turn-scope.port.js";
9
9
  /**
10
10
  * One admitted turn's services, over the turn's writer, built in this order:
11
11
  * the event bus and the tasks, with the writer as the bus's FIRST
12
- * subscriber (every stored event is queued before any reaction runs); then
13
- * the memory session, opened over the session's earlier memory events where
14
- * this layer is built (a digester or summarizer there is used), and
15
- * RunContext. It also holds the tools slot (`TurnToolbox`) and the prompt
16
- * state (`TurnPrompt`). The message was stored when the turn began; memory
12
+ * subscriber (every stored event is queued before any reaction runs) and
13
+ * the turn's usage (`RunContext.usage`) as the second; then the memory
14
+ * session, opened over the session's earlier memory events where this
15
+ * layer is built (a digester or summarizer there is used), and RunContext.
16
+ * It also holds the tools slot (`TurnToolbox`) and the prompt state
17
+ * (`TurnPrompt`). The message was stored when the turn began; memory
17
18
  * takes it at `TurnMemory.persistMessage`, and records nothing before it
18
19
  * (`TurnMemory.context` waits for it). Tasks run in the layer's scope:
19
20
  * they are interrupted when it closes.
@@ -1,4 +1,5 @@
1
1
  import { Context, Effect, Layer, Option, Ref, Schema, Scope, Semaphore } from "effect";
2
+ import { addUsage, zeroUsage } from "../domain/token-usage.entity.functions.js";
2
3
  import { HarnessError } from "../harness/plugin.entity.js";
3
4
  import { fingerprintOf } from "../memory/memory-log.entity.functions.js";
4
5
  import { readerOf } from "../memory/memory-session.js";
@@ -17,6 +18,8 @@ const failure = (code, message) => new HarnessError({ code, message }), inCaller
17
18
  export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
18
19
  const scope = yield* Effect.scope, memory = yield* ConversationMemory, registry = yield* ToolRegistry, sections = Option.getOrElse(yield* Effect.serviceOption(Capabilities), () => []).flatMap((capability) => capability.promptSections), writer = input.turn, admitted = writer.admitted, events = yield* makeTurnEvents({ maxDepth: input.maxEventDepth ?? 8 }), tasks = yield* makeTurnTasks(scope);
19
20
  yield* events.subscribe(storedDraft(admitted.turn), (draft) => draft instanceof HarnessError ? Effect.fail(draft) : writer.append([draft]));
21
+ const usage = yield* Ref.make(zeroUsage);
22
+ yield* events.subscribe((event) => event._tag === "assistant.message" ? Option.some(event.usage) : Option.none(), (step) => Ref.update(usage, (total) => addUsage(total, step)));
20
23
  const message = yield* Ref.make({ _tag: "Waiting", early: [] }), history = yield* Ref.make(Option.none()), decoded = (stored) => entriesOfEvents(stored).pipe(Effect.mapError((error) => failure("memory.log", `undecodable memory events: ${error.message}`))), readHistory = writer.history(MEMORY_KINDS).pipe(Effect.flatMap(decoded)), log = {
21
24
  read: readHistory.pipe(Effect.tap((entries) => Ref.set(history, Option.some(entries)))),
22
25
  append: (entries) => Effect.gen(function* () {
@@ -36,12 +39,14 @@ export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
36
39
  session: admitted.session,
37
40
  runId: admitted.runId,
38
41
  userMessage: admitted.userMessage,
42
+ admitted,
39
43
  memory: reader,
40
44
  events,
41
45
  tasks,
42
46
  activate: (skills) => openTools.pipe(Effect.flatMap((tools) => tools.activate(skills, "host"))),
43
47
  flush: writer.flush,
44
- write: writer.write
48
+ write: writer.write,
49
+ usage: Ref.get(usage)
45
50
  }), requestKinds = [...MEMORY_KINDS, "request.prepared"], snapshots = yield* Semaphore.make(1), snapshot = yield* Ref.make(Option.none()), requestSnapshot = snapshots.withPermits(1)(Effect.gen(function* () {
46
51
  const known = yield* Option.match(yield* Ref.get(snapshot), {
47
52
  onNone: () => Ref.get(history).pipe(Effect.flatMap(Option.match({ onNone: () => readHistory, onSome: Effect.succeed })), Effect.map((entries) => ({ entries, requests: [], cursor: writer.started.seq - 1 }))),
@@ -70,7 +75,14 @@ export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
70
75
  yield* session.record([{ _tag: "TurnStarted", userMessage: admitted.userMessage }], 0);
71
76
  const early = yield* Ref.modify(message, (state) => [state._tag === "Waiting" ? state.early : [], { _tag: "Taken", number }]);
72
77
  yield* early.length === 0 ? Effect.void : session.record(early.map(contextBody), 0);
73
- yield* events.publish({ _tag: "turn.started", runId: admitted.runId, turn: number, userMessage: admitted.userMessage });
78
+ yield* events.publish({
79
+ _tag: "turn.started",
80
+ runId: admitted.runId,
81
+ turn: number,
82
+ userMessage: admitted.userMessage,
83
+ meta: admitted.meta,
84
+ ...admitted.resumes === void 0 ? {} : { resumes: admitted.resumes }
85
+ });
74
86
  return number;
75
87
  }),
76
88
  context: (entry) => Ref.modify(message, (state) => state._tag === "Waiting" ? [!1, { _tag: "Waiting", early: [...state.early, entry] }] : [!0, state]).pipe(Effect.flatMap((now) => now ? session.record([contextBody(entry)], 0).pipe(Effect.asVoid) : Effect.void)),
@@ -79,7 +91,7 @@ export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
79
91
  if (Option.isNone(number) || (yield* Ref.getAndSet(replied, !0)))
80
92
  return;
81
93
  yield* session.record([{ _tag: "TurnEnded", outcome: outcome.outcome, reply: outcome.reply }], 0);
82
- yield* events.publish({ _tag: "turn.ended", runId: admitted.runId, turn: number.value, outcome: outcome.outcome, reply: outcome.reply });
94
+ yield* events.publish({ _tag: "turn.ended", runId: admitted.runId, turn: number.value, outcome: outcome.outcome, reply: outcome.reply, usage: yield* Ref.get(usage) });
83
95
  })
84
96
  }), opened = yield* Ref.make(!1), toolbox = TurnToolbox.of({
85
97
  open: Effect.gen(function* () {
@@ -9,12 +9,21 @@ import type { TurnRunner } from "../ports/turn.port.js";
9
9
  * the session's writer is the bus's first subscriber; the user's message
10
10
  * is in memory before the matcher reads the history; the reply is recorded
11
11
  * and the turn ended exactly once on success, failure and interrupt; tasks
12
- * are joined before the reply; and the host layer sees only earlier turns.
12
+ * are joined before the reply; a model-driven turn's `TurnResult.usage` is
13
+ * what its stored `step.usage` records sum to (`turnUsageOf`); the user's
14
+ * attachments reach the first request as image parts; and the host layer
15
+ * sees only earlier turns.
13
16
  *
14
17
  * `runner` is acquired afresh for each check. `services` are merged into
15
18
  * every turn's services (the model, the `Sessions` the turns begin with, and
16
19
  * whatever else the runner requires); each check makes its own session, and
17
20
  * the kit adds its own IntentMatcher, which the runner's tools must consult
18
- * (plugin-tool-discovery does).
21
+ * (plugin-tool-discovery does). The usage check runs the turn's loop twice
22
+ * with the kit's own model, which answers with text only, under a policy
23
+ * that completes after the first reply (`requireCompletion: false`, a
24
+ * complete verdict); its expected counts follow the calls the model answered.
25
+ * The attachments check runs the loop once with a model of its own that
26
+ * records each request; it needs the composition's memory to admit one small
27
+ * image (the memory plugins' defaults do; `maxMediaChars: 0` does not).
19
28
  */
20
29
  export declare const turnConformance: (runner: Effect.Effect<TurnRunner, HarnessError, Scope.Scope>, services?: Context.Context<never>) => ReadonlyArray<ConformanceCheck>;