@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, Effect, Layer, Option, Scope } from "effect";
|
|
2
2
|
import type { ConversationId } from "../domain/message.entity.js";
|
|
3
|
+
import type { TokenUsage } from "../domain/token-usage.entity.js";
|
|
3
4
|
import type { SkillDefinition } from "../harness/capability.entity.js";
|
|
4
5
|
import type { HarnessError } from "../harness/plugin.entity.js";
|
|
5
6
|
import type { CompletionVerdict } from "../turn/turn-event.entity.js";
|
|
@@ -9,7 +10,7 @@ import type { MemoryReader } from "./memory.port.js";
|
|
|
9
10
|
import type { RunContext } from "./run-context.port.js";
|
|
10
11
|
import type { TurnWriter } from "./sessions.port.js";
|
|
11
12
|
import type { JsonObject } from "../session/session-log.entity.js";
|
|
12
|
-
import type { SessionAddress } from "../session/sessions.entity.js";
|
|
13
|
+
import type { AdmittedTurn, SessionAddress } from "../session/sessions.entity.js";
|
|
13
14
|
import type { Correctives, LoopLimits, RunResult } from "./step-loop.port.js";
|
|
14
15
|
import type { SkillMatch } from "./tool-registry.port.js";
|
|
15
16
|
import type { TurnEvents, TurnEventsService, TurnTasks, TurnTasksService } from "./turn-events.port.js";
|
|
@@ -25,17 +26,46 @@ export interface TurnPolicy<R = never> {
|
|
|
25
26
|
/** Read-only by contract: react to events, decide here. */
|
|
26
27
|
readonly completion?: (step: StepInfo) => Effect.Effect<CompletionVerdict, HarnessError, R>;
|
|
27
28
|
readonly limits?: Partial<LoopLimits>;
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* This run's token budget (an integer of at least 1, else the run fails
|
|
31
|
+
* with `turn.budget`): the target memory compaction keeps each request
|
|
32
|
+
* under, the system prompt, tool schemas and messages estimated together,
|
|
33
|
+
* not a provider limit. The memory strategy is given the whole budget at
|
|
34
|
+
* the turn's start, and at each step what the system prompt and tool
|
|
35
|
+
* schemas leave of it (`context.budget` when they leave nothing): the
|
|
36
|
+
* window strategy compacts, spills and drops older turns to fit the budget
|
|
37
|
+
* it is given (and fails with `context.budget` when the current turn alone
|
|
38
|
+
* does not), the summary strategy summarizes once the messages pass its
|
|
39
|
+
* `triggerRatio` of it and keeps its `keepRatio`, and the digester ignores
|
|
40
|
+
* it. Overrides the agent's `budgetTokens` (64 000 by default).
|
|
41
|
+
*/
|
|
29
42
|
readonly budgetTokens?: number;
|
|
30
43
|
/** Where the step context goes: closing the messages, or appended to the system prompt. */
|
|
31
44
|
readonly stepContext?: "tail" | "system";
|
|
32
45
|
readonly correctives?: Correctives;
|
|
46
|
+
/**
|
|
47
|
+
* Whether this run's spans may carry prompt and response content
|
|
48
|
+
* (`engine.step.input`, `engine.step.output`, the models plugin's
|
|
49
|
+
* `llm.reasoning`). Overrides the agent's `captureTraceContent`; off by
|
|
50
|
+
* default. Hosts own consent, masking and exporter retention.
|
|
51
|
+
*/
|
|
52
|
+
readonly captureTraceContent?: boolean;
|
|
33
53
|
}
|
|
34
54
|
export interface TurnOutcome {
|
|
35
55
|
readonly outcome: "completed" | "partial" | "failed";
|
|
36
56
|
/** The reply follow-up turns remember. */
|
|
37
57
|
readonly reply: Option.Option<string>;
|
|
38
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* What a turn runner returns: the outcome `use` gave, and the turn's usage,
|
|
61
|
+
* the usage of its `assistant.message` events summed (the stored
|
|
62
|
+
* `step.usage` records of the turn sum to the same, see `turnUsageOf`).
|
|
63
|
+
* Calls outside the step loop (a memory digester's or summarizer's utility
|
|
64
|
+
* model) and responses that never decoded are not in it.
|
|
65
|
+
*/
|
|
66
|
+
export interface TurnResult extends TurnOutcome {
|
|
67
|
+
readonly usage: TokenUsage;
|
|
68
|
+
}
|
|
39
69
|
export interface TurnTools {
|
|
40
70
|
/** Ask the matcher without writing anything (run it beside other pre-turn work). */
|
|
41
71
|
readonly match: (userMessage: UserMessage) => Effect.Effect<SkillMatch, HarnessError>;
|
|
@@ -53,6 +83,8 @@ export interface Turn {
|
|
|
53
83
|
readonly runId: string;
|
|
54
84
|
readonly turn: number;
|
|
55
85
|
readonly userMessage: UserMessage;
|
|
86
|
+
/** The admitted turn as the session began it, the host's `meta` included (`RunContext.admitted`). */
|
|
87
|
+
readonly admitted: AdmittedTurn;
|
|
56
88
|
readonly memory: MemoryReader;
|
|
57
89
|
readonly events: TurnEventsService;
|
|
58
90
|
readonly tasks: TurnTasksService;
|
|
@@ -66,6 +98,8 @@ export interface Turn {
|
|
|
66
98
|
/** Answer without the loop; still a recorded turn. */
|
|
67
99
|
readonly reply: (text: string) => Effect.Effect<TurnOutcome>;
|
|
68
100
|
readonly run: <R = never>(policy: TurnPolicy<R>) => Effect.Effect<RunResult, HarnessError, R>;
|
|
101
|
+
/** The turn's usage so far, every run of it included (`RunContext.usage`). */
|
|
102
|
+
readonly usage: Effect.Effect<TokenUsage>;
|
|
69
103
|
/** Wait until every journal write queued so far is stored (e.g. before delivering an answer). */
|
|
70
104
|
readonly flush: Effect.Effect<void, HarnessError>;
|
|
71
105
|
/** Run a host store write in journal order and return its result. */
|
|
@@ -76,12 +110,34 @@ export type TurnServices = RunContext | TurnEvents | TurnTasks;
|
|
|
76
110
|
/** A turn to begin: the session (through the `Sessions` in the turn's services), the message and the run. */
|
|
77
111
|
export interface NewTurn {
|
|
78
112
|
readonly session: SessionAddress;
|
|
113
|
+
/** The user's message; when resuming, what the model sees after the resumed turn's steps (its own message again, or "Continue."). */
|
|
79
114
|
readonly userMessage: UserMessage;
|
|
80
115
|
readonly runId: string;
|
|
81
|
-
/** The idempotency key; the run id
|
|
116
|
+
/** The idempotency key; `resume:<n>` when resuming turn `n`, the run id otherwise. */
|
|
82
117
|
readonly key?: string;
|
|
83
118
|
/** The host's input beside the message, part of the key's fingerprint. */
|
|
84
119
|
readonly command?: JsonObject;
|
|
120
|
+
/**
|
|
121
|
+
* The host's metadata for the turn (a request id, a tenant…): stored in
|
|
122
|
+
* `turn.started`, shown on `RunContext.admitted`, `Turn.admitted` and the
|
|
123
|
+
* bus `turn.started`, and given to `TurnAdmission`. Not part of the key's
|
|
124
|
+
* fingerprint, and never shown to the model.
|
|
125
|
+
*/
|
|
126
|
+
readonly meta?: JsonObject;
|
|
127
|
+
/**
|
|
128
|
+
* Resume turn `n`: the session's last turn begun by a user, which ended
|
|
129
|
+
* cancelled or interrupted (only inbox turns, and resumes of it that
|
|
130
|
+
* failed, were cancelled or were interrupted, may have run since);
|
|
131
|
+
* otherwise, or when `n` is not a whole number of at least 1, the turn
|
|
132
|
+
* fails to begin with `turn.not-resumable`. Memory is what a plain new
|
|
133
|
+
* turn would see: what turn `n` stored (its message and whole steps), then
|
|
134
|
+
* this turn's message. Stored in `turn.started`, shown on `admitted` and
|
|
135
|
+
* the bus `turn.started`, and part of the key's fingerprint. With the
|
|
136
|
+
* default key `resume:<n>`, a retry is `turn.duplicate` once the resuming
|
|
137
|
+
* turn has begun, however it ended: to try a resume that failed again,
|
|
138
|
+
* pass a new key.
|
|
139
|
+
*/
|
|
140
|
+
readonly resumes?: number;
|
|
85
141
|
}
|
|
86
142
|
/**
|
|
87
143
|
* One turn's input. `turn` is either a turn the host already began (its
|
|
@@ -92,7 +148,16 @@ export interface NewTurn {
|
|
|
92
148
|
*/
|
|
93
149
|
export interface TurnInput<A = never, E = never> {
|
|
94
150
|
readonly turn: TurnWriter | NewTurn;
|
|
95
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* This turn's services: the model, per-turn budgets, data ports… They may
|
|
153
|
+
* be a captured context: `Agent.turn` provides them without their trace
|
|
154
|
+
* state (`replayableContext`), so a parent span or span annotations among
|
|
155
|
+
* them are ignored (the turn's spans nest under the span where
|
|
156
|
+
* `Agent.turn` is called; wrap that call in `Effect.withParentSpan` for
|
|
157
|
+
* another parent). A `CurrentTraceContent` among them is kept: it is
|
|
158
|
+
* where the turn's runs start, under the agent's and the policy's
|
|
159
|
+
* `captureTraceContent`.
|
|
160
|
+
*/
|
|
96
161
|
readonly services: Context.Context<never>;
|
|
97
162
|
/** A system prompt prefix before the contributed sections. */
|
|
98
163
|
readonly system?: string;
|
|
@@ -109,5 +174,5 @@ export interface TurnInput<A = never, E = never> {
|
|
|
109
174
|
* `turnConformance`).
|
|
110
175
|
*/
|
|
111
176
|
export interface TurnRunner {
|
|
112
|
-
readonly turn: <A = never, E = never, R = never>(input: TurnInput<A, E>, use: (turn: Turn) => Effect.Effect<TurnOutcome, HarnessError, R>) => Effect.Effect<
|
|
177
|
+
readonly turn: <A = never, E = never, R = never>(input: TurnInput<A, E>, use: (turn: Turn) => Effect.Effect<TurnOutcome, HarnessError, R>) => Effect.Effect<TurnResult, HarnessError | E, Exclude<R, A | TurnServices | Scope.Scope>>;
|
|
113
178
|
}
|
|
@@ -12,6 +12,8 @@ declare const UtilityCompletion_base: Schema.Class<UtilityCompletion, Schema.Str
|
|
|
12
12
|
readonly outputTokens: Schema.Number;
|
|
13
13
|
readonly totalTokens: Schema.Number;
|
|
14
14
|
readonly cacheReadTokens: Schema.Number;
|
|
15
|
+
readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
|
|
16
|
+
readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
|
|
15
17
|
}>;
|
|
16
18
|
}>, {}>;
|
|
17
19
|
export declare class UtilityCompletion extends UtilityCompletion_base {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A turn this process holds open: its session, its number, and since when
|
|
4
|
+
* (this process's Clock time, in milliseconds, when its begin was stored and
|
|
5
|
+
* its writer handed over).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ActiveTurn: Schema.Struct<{
|
|
8
|
+
readonly session: Schema.brand<Schema.String, "ConversationId">;
|
|
9
|
+
readonly turn: Schema.Int;
|
|
10
|
+
readonly since: Schema.Number;
|
|
11
|
+
}>;
|
|
12
|
+
export type ActiveTurn = typeof ActiveTurn.Type;
|
|
13
|
+
/**
|
|
14
|
+
* The turns a `Sessions` holds open and the begins it is still working on
|
|
15
|
+
* (waiting for the host's admission, committing, starting the writer),
|
|
16
|
+
* read at one moment.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TurnsInFlight: Schema.Struct<{
|
|
19
|
+
readonly open: Schema.$Array<Schema.Struct<{
|
|
20
|
+
readonly session: Schema.brand<Schema.String, "ConversationId">;
|
|
21
|
+
readonly turn: Schema.Int;
|
|
22
|
+
readonly since: Schema.Number;
|
|
23
|
+
}>>;
|
|
24
|
+
readonly beginning: Schema.Int;
|
|
25
|
+
}>;
|
|
26
|
+
export type TurnsInFlight = typeof TurnsInFlight.Type;
|
|
27
|
+
/**
|
|
28
|
+
* What this process is busy with, for a `/ping` or a graceful shutdown: the
|
|
29
|
+
* turns it holds open (oldest first), the begins still in progress, the
|
|
30
|
+
* background tasks it runs, and whether there is any of them.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ProcessActivity: Schema.Struct<{
|
|
33
|
+
readonly openTurns: Schema.$Array<Schema.Struct<{
|
|
34
|
+
readonly session: Schema.brand<Schema.String, "ConversationId">;
|
|
35
|
+
readonly turn: Schema.Int;
|
|
36
|
+
readonly since: Schema.Number;
|
|
37
|
+
}>>;
|
|
38
|
+
readonly beginningTurns: Schema.Int;
|
|
39
|
+
readonly runningTasks: Schema.Int;
|
|
40
|
+
readonly busy: Schema.Boolean;
|
|
41
|
+
}>;
|
|
42
|
+
export type ProcessActivity = typeof ProcessActivity.Type;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Duration } from "effect";
|
|
3
|
+
import type { ProcessActivity } from "./activity.entity.js";
|
|
4
|
+
/**
|
|
5
|
+
* What this process is busy with now: the turns its `TurnActivity` holds
|
|
6
|
+
* open or is beginning, and the work its `TaskActivity` runs. A probe that
|
|
7
|
+
* is not in the context counts as nothing open, beginning or running.
|
|
8
|
+
*/
|
|
9
|
+
export declare const processActivity: Effect.Effect<ProcessActivity>;
|
|
10
|
+
/**
|
|
11
|
+
* Wait until this process holds no turn open, begins none and runs no
|
|
12
|
+
* background work at the same moment, and return that activity (`busy:
|
|
13
|
+
* false`). With a `timeout`, stop waiting then and return the activity as
|
|
14
|
+
* it is (`busy: true`, with what is still in flight). It admits nothing
|
|
15
|
+
* itself: to drain before a shutdown, stop admitting new turns first (the
|
|
16
|
+
* host's `TurnAdmission`, or its own front door), then wait, then stop the
|
|
17
|
+
* host's own request fibers, then close the scope the services live in.
|
|
18
|
+
*/
|
|
19
|
+
export declare const awaitIdle: (options?: {
|
|
20
|
+
readonly timeout?: Duration.Input;
|
|
21
|
+
}) => Effect.Effect<ProcessActivity>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { TaskActivity, TurnActivity } from "../ports/activity.port.js";
|
|
3
|
+
const noTurns = { open: [], beginning: 0 };
|
|
4
|
+
export const processActivity = Effect.gen(function* () {
|
|
5
|
+
const turns = yield* Effect.serviceOption(TurnActivity), tasks = yield* Effect.serviceOption(TaskActivity), inFlight = yield* Option.match(turns, { onNone: () => Effect.succeed(noTurns), onSome: (probe) => probe.current }), runningTasks = yield* Option.match(tasks, { onNone: () => Effect.succeed(0), onSome: (probe) => probe.running });
|
|
6
|
+
return {
|
|
7
|
+
openTurns: inFlight.open,
|
|
8
|
+
beginningTurns: inFlight.beginning,
|
|
9
|
+
runningTasks,
|
|
10
|
+
busy: inFlight.open.length > 0 || inFlight.beginning > 0 || runningTasks > 0
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
const settled = Effect.gen(function* () {
|
|
14
|
+
const turns = yield* Effect.serviceOption(TurnActivity), tasks = yield* Effect.serviceOption(TaskActivity);
|
|
15
|
+
yield* Option.match(turns, { onNone: () => Effect.void, onSome: (probe) => probe.idle });
|
|
16
|
+
yield* Option.match(tasks, { onNone: () => Effect.void, onSome: (probe) => probe.idle });
|
|
17
|
+
const now = yield* processActivity;
|
|
18
|
+
return now.busy ? yield* Effect.yieldNow.pipe(Effect.andThen(settled)) : now;
|
|
19
|
+
});
|
|
20
|
+
export const awaitIdle = (options) => Option.match(Option.fromNullishOr(options?.timeout), {
|
|
21
|
+
onNone: () => settled,
|
|
22
|
+
onSome: (timeout) => Effect.timeoutOption(settled, timeout).pipe(Effect.flatMap(Option.match({ onNone: () => processActivity, onSome: Effect.succeed })))
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ConversationId } from "../domain/message.entity.js";
|
|
3
|
+
export const ActiveTurn = Schema.Struct({
|
|
4
|
+
session: ConversationId,
|
|
5
|
+
turn: Schema.Int,
|
|
6
|
+
since: Schema.Number
|
|
7
|
+
}), TurnsInFlight = Schema.Struct({
|
|
8
|
+
open: Schema.Array(ActiveTurn),
|
|
9
|
+
beginning: Schema.Int
|
|
10
|
+
}), ProcessActivity = Schema.Struct({
|
|
11
|
+
openTurns: Schema.Array(ActiveTurn),
|
|
12
|
+
beginningTurns: Schema.Int,
|
|
13
|
+
runningTasks: Schema.Int,
|
|
14
|
+
busy: Schema.Boolean
|
|
15
|
+
});
|
package/dist/session/chassis.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, Exit, Fiber, Option, PubSub, Ref, Stream, Semaphore } from "effect";
|
|
2
|
+
import { replayableContext } from "../loop/traceContent.js";
|
|
2
3
|
export const makeSession = (args) => Effect.gen(function* () {
|
|
3
|
-
const context = yield* Effect.context(), log = yield* Ref.make([]), hub = yield* PubSub.sliding(512), transientHub = yield* PubSub.sliding(128), running = yield* Ref.make(Option.none()), gate = yield* Semaphore.make(1), publish = (event) => args.isTransient?.(event) === !0 ? PubSub.publish(transientHub, event).pipe(Effect.asVoid) : Ref.modify(log, (entries) => {
|
|
4
|
+
const context = replayableContext(yield* Effect.context()), log = yield* Ref.make([]), hub = yield* PubSub.sliding(512), transientHub = yield* PubSub.sliding(128), running = yield* Ref.make(Option.none()), gate = yield* Semaphore.make(1), publish = (event) => args.isTransient?.(event) === !0 ? PubSub.publish(transientHub, event).pipe(Effect.asVoid) : Ref.modify(log, (entries) => {
|
|
4
5
|
const entry = { seq: entries.length, event };
|
|
5
6
|
return [entry, [...entries, entry]];
|
|
6
7
|
}).pipe(Effect.flatMap((entry) => PubSub.publish(hub, entry)), Effect.asVoid), runContained = (text) => args.runTurn(text, publish).pipe(Effect.provide(context), Effect.catch((error) => publish(args.onError(String(error)))), Effect.catchDefect((defect) => publish(args.onError(`turn crashed: ${String(defect)}`)))), send = (text) => gate.withPermits(1)(Effect.gen(function* () {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import type { TurnEventName } from "../turn/turn-event.entity.js";
|
|
2
3
|
import { UserMessage } from "../turn/user-message.entity.js";
|
|
3
4
|
/** Opens a turn: who asked, what they said, the host's command and the inbox items it claims. */
|
|
4
5
|
export declare const TurnStartedData: Schema.Struct<{
|
|
@@ -14,11 +15,30 @@ export declare const TurnStartedData: Schema.Struct<{
|
|
|
14
15
|
/** The memory entry the message is to the model, and when memory took it. */
|
|
15
16
|
readonly entry: Schema.brand<Schema.Trimmed, "EntryId">;
|
|
16
17
|
readonly at: Schema.Number;
|
|
18
|
+
/**
|
|
19
|
+
* The host's metadata for the turn (`BeginTurn.meta`), written only when
|
|
20
|
+
* given: not part of the key's fingerprint and never shown to the model.
|
|
21
|
+
*/
|
|
22
|
+
readonly meta: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
23
|
+
/**
|
|
24
|
+
* The earlier turn this one resumes (`BeginTurn.User.resumes`), written
|
|
25
|
+
* only when given: part of the key's fingerprint.
|
|
26
|
+
*/
|
|
27
|
+
readonly resumes: Schema.optionalKey<Schema.Int>;
|
|
17
28
|
}>;
|
|
18
29
|
export type TurnStartedData = typeof TurnStartedData.Type;
|
|
19
30
|
export declare const TurnEndReason: Schema.Literals<readonly ["completed", "partial", "failed", "cancelled", "interrupted"]>;
|
|
20
31
|
export type TurnEndReason = typeof TurnEndReason.Type;
|
|
21
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Closes a turn. Not model-visible: what later turns remember is `turn.reply`.
|
|
34
|
+
* `failure.code` is the failing error's own code when it has one (a
|
|
35
|
+
* `HarnessError`'s); otherwise one of the framework's codes, `turn.failed`
|
|
36
|
+
* (a typed error without a code), `turn.defect`, `turn.interrupted` (an
|
|
37
|
+
* interrupted turn or scope), `turn.unended` (a scope closed before its turn
|
|
38
|
+
* ended) or `turn.lease` (a reaped turn), or a code the closer passes to
|
|
39
|
+
* `writer.end` itself (plugin-tasks: `task.turn`, `task.deadline`; a host's
|
|
40
|
+
* own). A cancel stores none.
|
|
41
|
+
*/
|
|
22
42
|
export declare const TurnEndedData: Schema.Struct<{
|
|
23
43
|
readonly reason: Schema.Literals<readonly ["completed", "partial", "failed", "cancelled", "interrupted"]>;
|
|
24
44
|
readonly failure: Schema.OptionFromNullOr<Schema.Struct<{
|
|
@@ -27,6 +47,36 @@ export declare const TurnEndedData: Schema.Struct<{
|
|
|
27
47
|
}>>;
|
|
28
48
|
}>;
|
|
29
49
|
export type TurnEndedData = typeof TurnEndedData.Type;
|
|
50
|
+
/** An inbox item as delivered (`inbox.queued`): its stable id, who sent it and the text a turn shows the model. */
|
|
51
|
+
export declare const InboxQueuedData: Schema.Struct<{
|
|
52
|
+
readonly id: Schema.String;
|
|
53
|
+
readonly source: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
54
|
+
readonly content: Schema.String;
|
|
55
|
+
}>;
|
|
56
|
+
export type InboxQueuedData = typeof InboxQueuedData.Type;
|
|
57
|
+
/** An inbox item given up on (`inbox.dropped`): as many turns took it and failed as the inbox allows. */
|
|
58
|
+
export declare const InboxDroppedData: Schema.Struct<{
|
|
59
|
+
readonly id: Schema.String;
|
|
60
|
+
readonly reason: Schema.Literals<readonly ["attempts"]>;
|
|
61
|
+
}>;
|
|
62
|
+
export type InboxDroppedData = typeof InboxDroppedData.Type;
|
|
63
|
+
/**
|
|
64
|
+
* One step's provider response as stored (`step.usage`): its step, model and
|
|
65
|
+
* token usage. The rest of `assistant.message` is memory's `memory.message`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const StepUsageData: Schema.Struct<{
|
|
68
|
+
readonly step: Schema.Int;
|
|
69
|
+
readonly model: Schema.OptionFromNullOr<Schema.String>;
|
|
70
|
+
readonly usage: Schema.Struct<{
|
|
71
|
+
readonly inputTokens: Schema.Number;
|
|
72
|
+
readonly outputTokens: Schema.Number;
|
|
73
|
+
readonly totalTokens: Schema.Number;
|
|
74
|
+
readonly cacheReadTokens: Schema.Number;
|
|
75
|
+
readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
|
|
76
|
+
readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
|
|
77
|
+
}>;
|
|
78
|
+
}>;
|
|
79
|
+
export type StepUsageData = typeof StepUsageData.Type;
|
|
30
80
|
/** The kind each memory entry is stored under. `TurnStarted` is the turn's own `turn.started`. */
|
|
31
81
|
export declare const MemoryKindOf: {
|
|
32
82
|
readonly SystemPrepared: "memory.system";
|
|
@@ -43,13 +93,25 @@ export declare const MemoryKindOf: {
|
|
|
43
93
|
export declare const MEMORY_KINDS: ReadonlyArray<string>;
|
|
44
94
|
/** The turn's observable events stored from its bus (a host's records keep their own names). */
|
|
45
95
|
export declare const TURN_EVENT_KINDS: ReadonlyArray<string>;
|
|
96
|
+
/** Bus events not stored from the bus: memory or the session records them itself, and storing them again would be a second copy. */
|
|
97
|
+
export declare const RECORDED_ELSEWHERE: ReadonlySet<TurnEventName>;
|
|
46
98
|
/** The inbox and the session's own records. */
|
|
47
99
|
export declare const SESSION_KINDS: ReadonlyArray<string>;
|
|
48
100
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
101
|
+
* Every kind the framework writes to a session's journal: memory's, the
|
|
102
|
+
* bus events it stores, and the session's own. `journalSchemaOf(kind)` has
|
|
103
|
+
* the schema of each. `request.prepared` holds each request's full system
|
|
104
|
+
* text and tool declarations (and step context in "system" mode), next to
|
|
105
|
+
* the memory events: a host that redacts memory, or exports or streams
|
|
106
|
+
* session events, handles it the same way.
|
|
107
|
+
*/
|
|
108
|
+
export declare const JOURNAL_KINDS: ReadonlyArray<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Names only Efferent uses: a host event or a host record under one of
|
|
111
|
+
* these is refused. They are the kinds the framework writes
|
|
112
|
+
* (`JOURNAL_KINDS`) and every bus event's name (`TURN_EVENT_NAMES`), the
|
|
113
|
+
* ones never stored under their own name included (`assistant.message`,
|
|
114
|
+
* `assistant.delta`, `tool.progress`, `skills.activated`, `host`), so a host
|
|
115
|
+
* event never reads as one of the framework's.
|
|
54
116
|
*/
|
|
55
117
|
export declare const RESERVED_KINDS: ReadonlyArray<string>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { TokenUsage } from "../domain/token-usage.entity.js";
|
|
2
3
|
import { LogEntry } from "../memory/memory-log.entity.js";
|
|
3
4
|
import type { TurnEvent as TurnEventType } from "../turn/turn-event.entity.js";
|
|
4
5
|
import type { TurnEndedData as TurnEnded, TurnStartedData as TurnStarted } from "./session-event.entity.js";
|
|
@@ -18,14 +19,45 @@ export declare const turnEndedDraft: (turn: number, data: TurnEnded) => Effect.E
|
|
|
18
19
|
export declare const turnStartedOf: (event: SessionLogEvent) => Effect.Effect<TurnStarted, Schema.SchemaError>;
|
|
19
20
|
/** A stored `turn.ended`, decoded. */
|
|
20
21
|
export declare const turnEndedOf: (event: SessionLogEvent) => Effect.Effect<TurnEnded, Schema.SchemaError>;
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* One stored event → the memory entry it is, if it is one. A `turn.started`
|
|
24
|
+
* is decoded once: its user message, attachments included, is the entry's.
|
|
25
|
+
*/
|
|
22
26
|
export declare const entryOfEvent: (event: SessionLogEvent) => Effect.Effect<Option.Option<LogEntry>, Schema.SchemaError>;
|
|
23
27
|
/** The memory log held in a session's events, in log order. */
|
|
24
28
|
export declare const entriesOfEvents: (events: ReadonlyArray<SessionLogEvent>) => Effect.Effect<ReadonlyArray<LogEntry>, Schema.SchemaError>;
|
|
29
|
+
/**
|
|
30
|
+
* Turn `turn`'s usage from a session's stored events: its `step.usage`
|
|
31
|
+
* records summed, which is what the turn's `assistant.message` events
|
|
32
|
+
* carried (`TurnResult.usage`, `turn.ended.usage`). A record written before
|
|
33
|
+
* 0.14.0 reads 0 cache-write and reasoning tokens.
|
|
34
|
+
*/
|
|
35
|
+
export declare const turnUsageOf: (events: ReadonlyArray<SessionLogEvent>, turn: number) => Effect.Effect<TokenUsage, Schema.SchemaError>;
|
|
25
36
|
/**
|
|
26
37
|
* The stored form of one bus event of turn `turn`: its name and encoded
|
|
27
38
|
* payload. Transient events are not stored; a host's event keeps its name
|
|
28
39
|
* and data; an assistant message is stored only as the step's model and
|
|
29
|
-
* usage (its content is memory's `memory.message`).
|
|
40
|
+
* usage (its content is memory's `memory.message`); `tool.completed` leaves
|
|
41
|
+
* out its input and result, `tool.refused` its input.
|
|
30
42
|
*/
|
|
31
43
|
export declare const draftOfTurnEvent: (turn: number, event: TurnEventType) => Option.Option<SessionDraft>;
|
|
44
|
+
/**
|
|
45
|
+
* The stored form of a memory entry (`memory.*`, `turn.reply`): the entry's
|
|
46
|
+
* id, run, step and time, and `body`, the entry's body without its `_tag`
|
|
47
|
+
* (the kind names it, see `MemoryKindOf`).
|
|
48
|
+
*/
|
|
49
|
+
export declare const StoredMemoryData: <Body extends Schema.Top>(body: Body) => Schema.Struct<{
|
|
50
|
+
readonly entry: Schema.brand<Schema.Trimmed, "EntryId">;
|
|
51
|
+
readonly runId: Schema.String;
|
|
52
|
+
readonly step: Schema.Int;
|
|
53
|
+
readonly at: Schema.Number;
|
|
54
|
+
readonly body: Body;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* The schema the framework stores a journal kind's `data` with: every kind
|
|
58
|
+
* in `JOURNAL_KINDS` has one. None for any other kind: a host's records, a
|
|
59
|
+
* plugin's own (such as the tasks plugin's `task.*`), and the reserved bus
|
|
60
|
+
* names never stored under their own name (`assistant.message`,
|
|
61
|
+
* `tool.progress`…).
|
|
62
|
+
*/
|
|
63
|
+
export declare const journalSchemaOf: (kind: string) => Option.Option<Schema.Codec<unknown, unknown>>;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema, Struct } from "effect";
|
|
2
|
+
import { sumUsage } from "../domain/token-usage.entity.functions.js";
|
|
2
3
|
import { canonicalJson } from "../memory/memory-log.entity.functions.js";
|
|
3
|
-
import { LogEntry } from "../memory/memory-log.entity.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { EntryId, LogBody, LogEntry } from "../memory/memory-log.entity.js";
|
|
5
|
+
import { ModelRequestHeader } from "../turn/model-request.entity.js";
|
|
6
|
+
import { ContextBuiltEvent, ToolCompletedEvent, ToolRefusedEvent, TransientTurnEvents, TurnEvent } from "../turn/turn-event.entity.js";
|
|
7
|
+
import {
|
|
8
|
+
InboxDroppedData,
|
|
9
|
+
InboxQueuedData,
|
|
10
|
+
MemoryKindOf,
|
|
11
|
+
RECORDED_ELSEWHERE,
|
|
12
|
+
StepUsageData,
|
|
13
|
+
TURN_EVENT_KINDS,
|
|
14
|
+
TurnEndedData,
|
|
15
|
+
TurnStartedData
|
|
16
|
+
} from "./session-event.entity.js";
|
|
6
17
|
const encodeEntry = Schema.encodeEffect(LogEntry), decodeEntry = Schema.decodeUnknownEffect(LogEntry), encodeTurnEvent = Schema.encodeSync(TurnEvent), tagOfKind = new Map(Object.entries(MemoryKindOf).map(([tag, kind]) => [kind, tag])), canonical = (data) => JSON.parse(canonicalJson(data)), turnOf = (event) => Option.getOrElse(event.turn, () => 0);
|
|
7
18
|
export const draftOfEntry = (entry) => entry.body._tag === "TurnStarted" ? Effect.succeed(Option.none()) : encodeEntry(entry).pipe(Effect.map((encoded) => {
|
|
8
19
|
const { _tag, ...body } = encoded.body, kind = MemoryKindOf[_tag];
|
|
@@ -13,30 +24,57 @@ export const draftOfEntry = (entry) => entry.body._tag === "TurnStarted" ? Effec
|
|
|
13
24
|
});
|
|
14
25
|
})), draftsOfEntries = (entries) => Effect.forEach(entries, draftOfEntry).pipe(Effect.map((drafts) => drafts.flatMap(Option.toArray))), turnStartedDraft = (turn, data) => Schema.encodeEffect(TurnStartedData)(data).pipe(Effect.map((encoded) => ({ kind: "turn.started", turn: Option.some(turn), data: { ...encoded } }))), turnEndedDraft = (turn, data) => Schema.encodeEffect(TurnEndedData)(data).pipe(Effect.map((encoded) => ({ kind: "turn.ended", turn: Option.some(turn), data: { ...encoded } }))), turnStartedOf = (event) => Schema.decodeUnknownEffect(TurnStartedData)(canonical(event.data)), turnEndedOf = (event) => Schema.decodeUnknownEffect(TurnEndedData)(canonical(event.data)), entryOfEvent = (event) => {
|
|
15
26
|
if (event.kind === "turn.started")
|
|
16
|
-
return turnStartedOf(event).pipe(Effect.
|
|
27
|
+
return turnStartedOf(event).pipe(Effect.map((started) => Option.some({
|
|
17
28
|
id: started.entry,
|
|
18
29
|
runId: started.runId,
|
|
19
30
|
turn: turnOf(event),
|
|
20
31
|
step: 0,
|
|
21
32
|
at: started.at,
|
|
22
|
-
body: { _tag: "TurnStarted",
|
|
23
|
-
}))
|
|
33
|
+
body: { _tag: "TurnStarted", userMessage: started.userMessage }
|
|
34
|
+
})));
|
|
24
35
|
const tag = tagOfKind.get(event.kind);
|
|
25
36
|
if (tag === void 0)
|
|
26
37
|
return Effect.succeed(Option.none());
|
|
27
38
|
const data = canonical(event.data), body = typeof data.body === "object" && data.body !== null ? data.body : {};
|
|
28
39
|
return decodeEntry({ id: data.entry, runId: data.runId, turn: turnOf(event), step: data.step, at: data.at, body: { ...body, _tag: tag } }).pipe(Effect.map(Option.some));
|
|
29
40
|
}, entriesOfEvents = (events) => Effect.forEach(events, entryOfEvent).pipe(Effect.map((entries) => entries.flatMap(Option.toArray)));
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
const OMITTED_FROM_TOOL_COMPLETED = ["result", "encoded", "input"], OMITTED_FROM_TOOL_REFUSED = ["input"], without = (omitted) => (data) => Object.fromEntries(Object.entries(data).filter(([key]) => !omitted.includes(key))), storedPayload = new Map([
|
|
42
|
+
["tool.completed", without(OMITTED_FROM_TOOL_COMPLETED)],
|
|
43
|
+
["tool.refused", without(OMITTED_FROM_TOOL_REFUSED)]
|
|
44
|
+
]), encodeStepUsage = Schema.encodeSync(StepUsageData), decodeStepUsage = Schema.decodeUnknownEffect(StepUsageData);
|
|
45
|
+
export const turnUsageOf = (events, turn) => Effect.forEach(events.filter((event) => event.kind === "step.usage" && Option.contains(event.turn, turn)), (event) => decodeStepUsage(canonical(event.data))).pipe(Effect.map((records) => sumUsage(records.map((record) => record.usage)))), draftOfTurnEvent = (turn, event) => {
|
|
46
|
+
if (TransientTurnEvents.includes(event._tag) || RECORDED_ELSEWHERE.has(event._tag))
|
|
33
47
|
return Option.none();
|
|
34
48
|
if (event._tag === "host")
|
|
35
49
|
return Option.some({ kind: event.name, turn: Option.some(turn), data: event.data });
|
|
50
|
+
if (event._tag === "assistant.message")
|
|
51
|
+
return Option.some({ kind: "step.usage", turn: Option.some(turn), data: { ...encodeStepUsage({ step: event.step, model: event.model, usage: event.usage }) } });
|
|
36
52
|
const { _tag, ...data } = encodeTurnEvent(event);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
return Option.some({ kind: _tag, turn: Option.some(turn), data: Option.match(Option.fromNullishOr(storedPayload.get(_tag)), { onNone: () => data, onSome: (stored) => stored(data) }) });
|
|
54
|
+
}, StoredMemoryData = (body) => Schema.Struct({
|
|
55
|
+
entry: EntryId,
|
|
56
|
+
runId: Schema.String,
|
|
57
|
+
step: Schema.Int,
|
|
58
|
+
at: Schema.Number,
|
|
59
|
+
body
|
|
60
|
+
});
|
|
61
|
+
const memorySchemas = LogBody.members.flatMap((member) => ("fields" in member) ? [[MemoryKindOf[member.fields._tag.schema.literal], StoredMemoryData(Schema.Struct(Struct.omit(member.fields, ["_tag"])))]] : []), laterKeys = new Map([
|
|
62
|
+
["context.built", Schema.Struct(Struct.evolve(Struct.omit(ContextBuiltEvent.fields, ["_tag"]), { budgetTokens: (field) => Schema.optionalKey(field) }))]
|
|
63
|
+
]), busSchemas = TurnEvent.members.flatMap((member) => {
|
|
64
|
+
const tag = member.fields._tag.schema.literal;
|
|
65
|
+
if (storedPayload.has(tag) || !TURN_EVENT_KINDS.includes(tag))
|
|
66
|
+
return [];
|
|
67
|
+
return [[tag, laterKeys.get(tag) ?? Schema.Struct(Struct.omit(member.fields, ["_tag"]))]];
|
|
68
|
+
}), journalSchemas = new Map([
|
|
69
|
+
["turn.started", TurnStartedData],
|
|
70
|
+
["turn.ended", TurnEndedData],
|
|
71
|
+
["step.usage", StepUsageData],
|
|
72
|
+
["request.prepared", ModelRequestHeader],
|
|
73
|
+
["inbox.queued", InboxQueuedData],
|
|
74
|
+
["inbox.dropped", InboxDroppedData],
|
|
75
|
+
["tool.completed", Schema.Struct(Struct.omit(ToolCompletedEvent.fields, ["_tag", ...OMITTED_FROM_TOOL_COMPLETED]))],
|
|
76
|
+
["tool.refused", Schema.Struct(Struct.omit(ToolRefusedEvent.fields, ["_tag", ...OMITTED_FROM_TOOL_REFUSED]))],
|
|
77
|
+
...memorySchemas,
|
|
78
|
+
...busSchemas
|
|
79
|
+
]);
|
|
80
|
+
export const journalSchemaOf = (kind) => Option.fromNullishOr(journalSchemas.get(kind));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { TokenUsage } from "../domain/token-usage.entity.js";
|
|
2
3
|
import { EntryId } from "../memory/memory-log.entity.js";
|
|
4
|
+
import { TURN_EVENT_NAMES } from "../turn/turn-event.entity.js";
|
|
3
5
|
import { UserMessage } from "../turn/user-message.entity.js";
|
|
4
6
|
import { JsonObject } from "./session-log.entity.js";
|
|
5
7
|
export const TurnStartedData = Schema.Struct({
|
|
@@ -10,10 +12,23 @@ export const TurnStartedData = Schema.Struct({
|
|
|
10
12
|
command: JsonObject,
|
|
11
13
|
claimed: Schema.Array(Schema.String),
|
|
12
14
|
entry: EntryId,
|
|
13
|
-
at: Schema.Number
|
|
15
|
+
at: Schema.Number,
|
|
16
|
+
meta: Schema.optionalKey(JsonObject),
|
|
17
|
+
resumes: Schema.optionalKey(Schema.Int)
|
|
14
18
|
}), TurnEndReason = Schema.Literals(["completed", "partial", "failed", "cancelled", "interrupted"]), TurnEndedData = Schema.Struct({
|
|
15
19
|
reason: TurnEndReason,
|
|
16
20
|
failure: Schema.OptionFromNullOr(Schema.Struct({ code: Schema.String, message: Schema.String }))
|
|
21
|
+
}), InboxQueuedData = Schema.Struct({
|
|
22
|
+
id: Schema.String,
|
|
23
|
+
source: JsonObject,
|
|
24
|
+
content: Schema.String
|
|
25
|
+
}), InboxDroppedData = Schema.Struct({
|
|
26
|
+
id: Schema.String,
|
|
27
|
+
reason: Schema.Literals(["attempts"])
|
|
28
|
+
}), StepUsageData = Schema.Struct({
|
|
29
|
+
step: Schema.Int,
|
|
30
|
+
model: Schema.OptionFromNullOr(Schema.String),
|
|
31
|
+
usage: TokenUsage
|
|
17
32
|
}), MemoryKindOf = {
|
|
18
33
|
SystemPrepared: "memory.system",
|
|
19
34
|
TurnContext: "memory.section",
|
|
@@ -29,9 +44,11 @@ export const TurnStartedData = Schema.Struct({
|
|
|
29
44
|
"step.ended",
|
|
30
45
|
"step.usage",
|
|
31
46
|
"tool.started",
|
|
47
|
+
"tool.refused",
|
|
32
48
|
"tool.completed",
|
|
33
49
|
"completion.evaluated",
|
|
34
50
|
"context.built",
|
|
35
51
|
"decision.recorded",
|
|
52
|
+
"stream.fallback",
|
|
36
53
|
"request.prepared"
|
|
37
|
-
], SESSION_KINDS = ["turn.ended", "inbox.queued", "inbox.dropped"],
|
|
54
|
+
], RECORDED_ELSEWHERE = new Set(["turn.started", "turn.ended", "skills.activated"]), SESSION_KINDS = ["turn.ended", "inbox.queued", "inbox.dropped"], JOURNAL_KINDS = [...MEMORY_KINDS, ...TURN_EVENT_KINDS, ...SESSION_KINDS], RESERVED_KINDS = [...JOURNAL_KINDS, ...TURN_EVENT_NAMES.filter((name) => !JOURNAL_KINDS.includes(name))];
|
|
@@ -5,9 +5,10 @@ type Log = Context.Service.Shape<typeof SessionLog>;
|
|
|
5
5
|
/**
|
|
6
6
|
* The SessionLog contract, as checks any backend must pass: dense sequence
|
|
7
7
|
* numbers, one revision per commit, compare-and-swap, the `notAfter` clock
|
|
8
|
-
* check, JSON kept by value,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* check, JSON kept by value, an event as large as a turn's images may make
|
|
9
|
+
* it (`MEDIA_LIMITS.requestChars` base64 characters), reads by position and
|
|
10
|
+
* kind, listing order, children and their removal. Every check uses fresh
|
|
11
|
+
* ids and a fresh owner, so the kit is safe on a shared database.
|
|
11
12
|
*/
|
|
12
13
|
export declare const sessionLogConformance: (log: Log) => ReadonlyArray<ConformanceCheck>;
|
|
13
14
|
export {};
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { check, expect, freshId, headerOf } from "../conformance.functions.js";
|
|
3
|
+
import { MEDIA_LIMITS } from "../domain/media.entity.js";
|
|
4
4
|
import { canonicalJson } from "../memory/memory-log.entity.functions.js";
|
|
5
|
-
const
|
|
6
|
-
id,
|
|
7
|
-
owner,
|
|
8
|
-
origin: "user",
|
|
9
|
-
createdAt: 1767225600000,
|
|
10
|
-
meta: { title: "conformance", nested: { list: [1, "two", null] } },
|
|
11
|
-
parent
|
|
12
|
-
}), draft = (kind, data, turn = Option.none()) => ({ kind, turn, data }), noEvents = { notAfter: Option.none(), state: Option.none() }, rich = { zeta: 1, alpha: { b: [!0, !1, { y: "\xFC", x: 0.5 }], a: "text \u2713" }, empty: {}, list: [] };
|
|
5
|
+
const draft = (kind, data, turn = Option.none()) => ({ kind, turn, data }), noEvents = { notAfter: Option.none(), state: Option.none() }, rich = { zeta: 1, alpha: { b: [!0, !1, { y: "\xFC", x: 0.5 }], a: "text \u2713" }, empty: {}, list: [] };
|
|
13
6
|
export const sessionLogConformance = (log) => {
|
|
14
|
-
const check = (name, id, body) => ({
|
|
15
|
-
name,
|
|
16
|
-
run: freshId.pipe(Effect.flatMap((owner) => body(`owner-${owner}`)), Effect.catch((error) => error instanceof ConformanceFailure ? Effect.fail(error) : fail(id, `unexpected ${typeof error === "object" && error !== null && "_tag" in error ? String(error._tag) : "error"}: ${JSON.stringify(error)}`)))
|
|
17
|
-
});
|
|
18
7
|
return [
|
|
19
8
|
check("a new session is empty: seq 0, revision 0, the header as given", "create", (owner) => Effect.gen(function* () {
|
|
20
9
|
const id = yield* freshId, created = yield* log.create(headerOf(id, owner)), head = yield* log.head(id);
|
|
@@ -79,6 +68,19 @@ export const sessionLogConformance = (log) => {
|
|
|
79
68
|
yield* expect("json", event !== void 0 && canonicalJson(event.data) === canonicalJson(rich), `data came back as ${JSON.stringify(event?.data)}`);
|
|
80
69
|
yield* expect("json", canonicalJson((yield* log.head(id)).state) === canonicalJson(rich), "the state came back different");
|
|
81
70
|
})),
|
|
71
|
+
check("an event holding MEDIA_LIMITS.requestChars of base64 (a message's images at their limit) comes back whole", "large", (owner) => Effect.gen(function* () {
|
|
72
|
+
const id = yield* freshId;
|
|
73
|
+
yield* log.create(headerOf(id, owner));
|
|
74
|
+
const each = MEDIA_LIMITS.requestChars / MEDIA_LIMITS.attachments, large = {
|
|
75
|
+
userMessage: {
|
|
76
|
+
text: "large",
|
|
77
|
+
attachments: Array.from({ length: MEDIA_LIMITS.attachments }, (_, index) => ({ type: "media", data: `${"QUJD".repeat(each / 4 - 1)}${String(index).padStart(4, "0")}` }))
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
yield* log.commit(id, { expect: 0, ...noEvents, events: [draft("large", large, Option.some(1))] });
|
|
81
|
+
const [event] = yield* log.read(id, { after: 0, limit: Option.none(), kinds: [] }), chars = (data) => canonicalJson(data ?? {}).length;
|
|
82
|
+
yield* expect("large", event !== void 0 && chars(event.data) === chars(large) && canonicalJson(event.data) === canonicalJson(large), `an event of ${chars(large)} characters came back with ${chars(event?.data)}`);
|
|
83
|
+
})),
|
|
82
84
|
check("input and returned JSON are detached from stored heads and events", "json-isolation", (owner) => Effect.gen(function* () {
|
|
83
85
|
const id = yield* freshId, header = headerOf(id, owner), expectedMeta = canonicalJson(header.meta), created = yield* log.create(header), change = (json, by) => {
|
|
84
86
|
Reflect.set(json, "changed", by);
|