@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.
- package/README.md +1 -1
- package/dist/conformance.functions.d.ts +34 -0
- package/dist/conformance.functions.js +25 -0
- package/dist/domain/bash.entity.d.ts +82 -0
- package/dist/domain/bash.entity.functions.d.ts +17 -0
- package/dist/domain/bash.entity.functions.js +26 -0
- package/dist/domain/bash.entity.functions.test.d.ts +1 -0
- package/dist/domain/bash.entity.js +30 -0
- package/dist/domain/file-system.entity.d.ts +91 -0
- package/dist/domain/file-system.entity.functions.d.ts +18 -0
- package/dist/domain/file-system.entity.functions.js +30 -0
- package/dist/domain/file-system.entity.js +37 -0
- package/dist/domain/loop-event.entity.d.ts +9 -0
- package/dist/domain/media.entity.d.ts +79 -0
- package/dist/domain/media.entity.functions.d.ts +76 -0
- package/dist/domain/media.entity.functions.js +110 -0
- package/dist/domain/media.entity.js +38 -0
- package/dist/domain/media.entity.test.d.ts +1 -0
- package/dist/domain/message.entity.d.ts +157 -5
- package/dist/domain/message.entity.js +8 -4
- package/dist/domain/model-catalog.entity.d.ts +2 -0
- package/dist/domain/model-catalog.entity.js +2 -1
- package/dist/domain/token-usage.entity.d.ts +45 -1
- package/dist/domain/token-usage.entity.functions.d.ts +30 -1
- package/dist/domain/token-usage.entity.functions.js +24 -4
- package/dist/domain/token-usage.entity.js +19 -2
- package/dist/domain/token-usage.entity.test.d.ts +1 -0
- package/dist/harness/capability.entity.functions.js +2 -1
- package/dist/harness/plugin.adapter.d.ts +3 -0
- package/dist/harness/plugin.adapter.js +7 -1
- package/dist/harness/skill-files.entity.d.ts +101 -0
- package/dist/harness/skill-files.entity.functions.d.ts +58 -0
- package/dist/harness/skill-files.entity.functions.js +150 -0
- package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
- package/dist/harness/skill-files.entity.js +25 -0
- package/dist/index.d.ts +48 -8
- package/dist/index.js +67 -6
- package/dist/loop/mapping.d.ts +12 -6
- package/dist/loop/mapping.js +30 -6
- package/dist/loop/stepContext.d.ts +8 -0
- package/dist/loop/stepContext.js +1 -1
- package/dist/loop/traceContent.d.ts +29 -0
- package/dist/loop/traceContent.js +4 -0
- package/dist/loop/traceContent.test.d.ts +1 -0
- package/dist/memory/memory-log.entity.d.ts +152 -14
- package/dist/memory/memory-log.entity.functions.d.ts +35 -5
- package/dist/memory/memory-log.entity.functions.js +59 -20
- package/dist/memory/memory-log.entity.js +19 -6
- package/dist/memory/memory-session.js +10 -6
- package/dist/memory/memory.conformance.d.ts +8 -1
- package/dist/memory/memory.conformance.js +32 -0
- package/dist/models/auth-store.conformance.d.ts +13 -0
- package/dist/models/auth-store.conformance.js +73 -0
- package/dist/models/auth-store.conformance.test.d.ts +1 -0
- package/dist/models/model-catalog.conformance.d.ts +15 -0
- package/dist/models/model-catalog.conformance.js +27 -0
- package/dist/models/model-catalog.conformance.test.d.ts +1 -0
- package/dist/models/settings-store.conformance.d.ts +26 -0
- package/dist/models/settings-store.conformance.js +80 -0
- package/dist/models/settings-store.conformance.test.d.ts +1 -0
- package/dist/ports/activity.port.d.ts +35 -0
- package/dist/ports/activity.port.js +7 -0
- package/dist/ports/auth-store.port.d.ts +7 -5
- package/dist/ports/bash.port.d.ts +22 -0
- package/dist/ports/bash.port.js +4 -0
- package/dist/ports/capability.port.d.ts +11 -1
- package/dist/ports/file-system.port.d.ts +44 -15
- package/dist/ports/file-system.port.js +1 -7
- package/dist/ports/memory.port.d.ts +13 -1
- package/dist/ports/model-catalog.port.d.ts +4 -1
- package/dist/ports/run-context.port.d.ts +6 -1
- package/dist/ports/session-log.port.d.ts +4 -1
- package/dist/ports/sessions.port.d.ts +35 -6
- package/dist/ports/settings-store.port.d.ts +11 -7
- package/dist/ports/step-loop.port.d.ts +30 -2
- package/dist/ports/tasks.port.d.ts +5 -1
- package/dist/ports/turn-admission.port.d.ts +8 -2
- package/dist/ports/turn-events.port.d.ts +8 -1
- package/dist/ports/turn-scope.port.d.ts +19 -1
- package/dist/ports/turn.port.d.ts +70 -5
- package/dist/ports/utility-llm.port.d.ts +2 -0
- package/dist/session/activity.entity.d.ts +42 -0
- package/dist/session/activity.entity.functions.d.ts +21 -0
- package/dist/session/activity.entity.functions.js +23 -0
- package/dist/session/activity.entity.functions.test.d.ts +1 -0
- package/dist/session/activity.entity.js +15 -0
- package/dist/session/chassis.js +2 -1
- package/dist/session/session-event.entity.d.ts +68 -6
- package/dist/session/session-event.entity.functions.d.ts +34 -2
- package/dist/session/session-event.entity.functions.js +54 -16
- package/dist/session/session-event.entity.js +19 -2
- package/dist/session/session-log.conformance.d.ts +4 -3
- package/dist/session/session-log.conformance.js +16 -14
- package/dist/session/session-log.recovery.conformance.d.ts +40 -0
- package/dist/session/session-log.recovery.conformance.js +143 -0
- package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
- package/dist/session/sessions.conformance.d.ts +79 -0
- package/dist/session/sessions.conformance.js +355 -0
- package/dist/session/sessions.entity.d.ts +36 -3
- package/dist/session/sessions.entity.functions.d.ts +42 -0
- package/dist/session/sessions.entity.functions.js +26 -0
- package/dist/session/sessions.entity.js +17 -4
- package/dist/session/sessions.entity.test.d.ts +1 -0
- package/dist/session/tasks.conformance.d.ts +70 -0
- package/dist/session/tasks.conformance.js +213 -0
- package/dist/session/tasks.conformance.test.d.ts +1 -0
- package/dist/turn/model-request.entity.d.ts +4 -0
- package/dist/turn/model-request.entity.js +7 -2
- package/dist/turn/step-loop.conformance.d.ts +4 -1
- package/dist/turn/step-loop.conformance.js +73 -7
- package/dist/turn/tool-registry.conformance.d.ts +38 -0
- package/dist/turn/tool-registry.conformance.js +206 -0
- package/dist/turn/turn-bus.js +4 -3
- package/dist/turn/turn-event.entity.d.ts +134 -34
- package/dist/turn/turn-event.entity.functions.d.ts +11 -0
- package/dist/turn/turn-event.entity.functions.js +9 -1
- package/dist/turn/turn-event.entity.js +76 -3
- package/dist/turn/turn-event.entity.test.d.ts +1 -0
- package/dist/turn/turn-lifecycle.d.ts +46 -8
- package/dist/turn/turn-lifecycle.js +33 -3
- package/dist/turn/turn-lifecycle.test.d.ts +1 -0
- package/dist/turn/turn-run.d.ts +15 -3
- package/dist/turn/turn-run.js +37 -9
- package/dist/turn/turn.adapter.d.ts +6 -5
- package/dist/turn/turn.adapter.js +15 -3
- package/dist/turn/turn.conformance.d.ts +11 -2
- package/dist/turn/turn.conformance.js +63 -2
- package/dist/turn/user-message.entity.d.ts +14 -4
- package/dist/turn/user-message.entity.functions.d.ts +21 -0
- package/dist/turn/user-message.entity.functions.js +24 -0
- package/dist/turn/user-message.entity.js +6 -8
- package/dist/turn/user-message.entity.test.d.ts +1 -0
- package/dist/util/glob.d.ts +30 -0
- package/dist/util/glob.js +91 -0
- package/dist/util/glob.test.d.ts +1 -0
- package/dist/workspace/bash.conformance.d.ts +37 -0
- package/dist/workspace/bash.conformance.js +120 -0
- package/dist/workspace/bash.conformance.test.d.ts +1 -0
- package/dist/workspace/file-system.conformance.d.ts +38 -0
- package/dist/workspace/file-system.conformance.js +254 -0
- package/dist/workspace/file-system.conformance.test.d.ts +1 -0
- package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
- package/dist/workspace/file-system.memory.adapter.js +132 -0
- package/package.json +1 -1
- package/dist/harness/skill-files.d.ts +0 -17
- package/dist/harness/skill-files.js +0 -25
- package/dist/ports/shell.port.d.ts +0 -32
- 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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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<
|
|
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
|
-
*
|
|
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 {};
|
package/dist/turn/turn-run.d.ts
CHANGED
|
@@ -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
|
|
19
|
-
*
|
|
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. */
|
package/dist/turn/turn-run.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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)
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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({
|
|
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;
|
|
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>;
|