@xandreed/core 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.functions.d.ts +34 -0
  3. package/dist/conformance.functions.js +25 -0
  4. package/dist/domain/bash.entity.d.ts +82 -0
  5. package/dist/domain/bash.entity.functions.d.ts +17 -0
  6. package/dist/domain/bash.entity.functions.js +26 -0
  7. package/dist/domain/bash.entity.functions.test.d.ts +1 -0
  8. package/dist/domain/bash.entity.js +30 -0
  9. package/dist/domain/file-system.entity.d.ts +91 -0
  10. package/dist/domain/file-system.entity.functions.d.ts +18 -0
  11. package/dist/domain/file-system.entity.functions.js +30 -0
  12. package/dist/domain/file-system.entity.js +37 -0
  13. package/dist/domain/loop-event.entity.d.ts +9 -0
  14. package/dist/domain/media.entity.d.ts +79 -0
  15. package/dist/domain/media.entity.functions.d.ts +76 -0
  16. package/dist/domain/media.entity.functions.js +110 -0
  17. package/dist/domain/media.entity.js +38 -0
  18. package/dist/domain/media.entity.test.d.ts +1 -0
  19. package/dist/domain/message.entity.d.ts +157 -5
  20. package/dist/domain/message.entity.js +8 -4
  21. package/dist/domain/model-catalog.entity.d.ts +2 -0
  22. package/dist/domain/model-catalog.entity.js +2 -1
  23. package/dist/domain/token-usage.entity.d.ts +45 -1
  24. package/dist/domain/token-usage.entity.functions.d.ts +30 -1
  25. package/dist/domain/token-usage.entity.functions.js +24 -4
  26. package/dist/domain/token-usage.entity.js +19 -2
  27. package/dist/domain/token-usage.entity.test.d.ts +1 -0
  28. package/dist/harness/capability.entity.functions.js +2 -1
  29. package/dist/harness/plugin.adapter.d.ts +3 -0
  30. package/dist/harness/plugin.adapter.js +7 -1
  31. package/dist/harness/skill-files.entity.d.ts +101 -0
  32. package/dist/harness/skill-files.entity.functions.d.ts +58 -0
  33. package/dist/harness/skill-files.entity.functions.js +150 -0
  34. package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
  35. package/dist/harness/skill-files.entity.js +25 -0
  36. package/dist/index.d.ts +48 -8
  37. package/dist/index.js +67 -6
  38. package/dist/loop/mapping.d.ts +12 -6
  39. package/dist/loop/mapping.js +30 -6
  40. package/dist/loop/stepContext.d.ts +8 -0
  41. package/dist/loop/stepContext.js +1 -1
  42. package/dist/loop/traceContent.d.ts +29 -0
  43. package/dist/loop/traceContent.js +4 -0
  44. package/dist/loop/traceContent.test.d.ts +1 -0
  45. package/dist/memory/memory-log.entity.d.ts +152 -14
  46. package/dist/memory/memory-log.entity.functions.d.ts +35 -5
  47. package/dist/memory/memory-log.entity.functions.js +59 -20
  48. package/dist/memory/memory-log.entity.js +19 -6
  49. package/dist/memory/memory-session.js +10 -6
  50. package/dist/memory/memory.conformance.d.ts +8 -1
  51. package/dist/memory/memory.conformance.js +32 -0
  52. package/dist/models/auth-store.conformance.d.ts +13 -0
  53. package/dist/models/auth-store.conformance.js +73 -0
  54. package/dist/models/auth-store.conformance.test.d.ts +1 -0
  55. package/dist/models/model-catalog.conformance.d.ts +15 -0
  56. package/dist/models/model-catalog.conformance.js +27 -0
  57. package/dist/models/model-catalog.conformance.test.d.ts +1 -0
  58. package/dist/models/settings-store.conformance.d.ts +26 -0
  59. package/dist/models/settings-store.conformance.js +80 -0
  60. package/dist/models/settings-store.conformance.test.d.ts +1 -0
  61. package/dist/ports/activity.port.d.ts +35 -0
  62. package/dist/ports/activity.port.js +7 -0
  63. package/dist/ports/auth-store.port.d.ts +7 -5
  64. package/dist/ports/bash.port.d.ts +22 -0
  65. package/dist/ports/bash.port.js +4 -0
  66. package/dist/ports/capability.port.d.ts +11 -1
  67. package/dist/ports/file-system.port.d.ts +44 -15
  68. package/dist/ports/file-system.port.js +1 -7
  69. package/dist/ports/memory.port.d.ts +13 -1
  70. package/dist/ports/model-catalog.port.d.ts +4 -1
  71. package/dist/ports/run-context.port.d.ts +6 -1
  72. package/dist/ports/session-log.port.d.ts +4 -1
  73. package/dist/ports/sessions.port.d.ts +35 -6
  74. package/dist/ports/settings-store.port.d.ts +11 -7
  75. package/dist/ports/step-loop.port.d.ts +30 -2
  76. package/dist/ports/tasks.port.d.ts +5 -1
  77. package/dist/ports/turn-admission.port.d.ts +8 -2
  78. package/dist/ports/turn-events.port.d.ts +8 -1
  79. package/dist/ports/turn-scope.port.d.ts +19 -1
  80. package/dist/ports/turn.port.d.ts +70 -5
  81. package/dist/ports/utility-llm.port.d.ts +2 -0
  82. package/dist/session/activity.entity.d.ts +42 -0
  83. package/dist/session/activity.entity.functions.d.ts +21 -0
  84. package/dist/session/activity.entity.functions.js +23 -0
  85. package/dist/session/activity.entity.functions.test.d.ts +1 -0
  86. package/dist/session/activity.entity.js +15 -0
  87. package/dist/session/chassis.js +2 -1
  88. package/dist/session/session-event.entity.d.ts +68 -6
  89. package/dist/session/session-event.entity.functions.d.ts +34 -2
  90. package/dist/session/session-event.entity.functions.js +54 -16
  91. package/dist/session/session-event.entity.js +19 -2
  92. package/dist/session/session-log.conformance.d.ts +4 -3
  93. package/dist/session/session-log.conformance.js +16 -14
  94. package/dist/session/session-log.recovery.conformance.d.ts +40 -0
  95. package/dist/session/session-log.recovery.conformance.js +143 -0
  96. package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
  97. package/dist/session/sessions.conformance.d.ts +79 -0
  98. package/dist/session/sessions.conformance.js +355 -0
  99. package/dist/session/sessions.entity.d.ts +36 -3
  100. package/dist/session/sessions.entity.functions.d.ts +42 -0
  101. package/dist/session/sessions.entity.functions.js +26 -0
  102. package/dist/session/sessions.entity.js +17 -4
  103. package/dist/session/sessions.entity.test.d.ts +1 -0
  104. package/dist/session/tasks.conformance.d.ts +70 -0
  105. package/dist/session/tasks.conformance.js +213 -0
  106. package/dist/session/tasks.conformance.test.d.ts +1 -0
  107. package/dist/turn/model-request.entity.d.ts +4 -0
  108. package/dist/turn/model-request.entity.js +7 -2
  109. package/dist/turn/step-loop.conformance.d.ts +4 -1
  110. package/dist/turn/step-loop.conformance.js +73 -7
  111. package/dist/turn/tool-registry.conformance.d.ts +38 -0
  112. package/dist/turn/tool-registry.conformance.js +206 -0
  113. package/dist/turn/turn-bus.js +4 -3
  114. package/dist/turn/turn-event.entity.d.ts +134 -34
  115. package/dist/turn/turn-event.entity.functions.d.ts +11 -0
  116. package/dist/turn/turn-event.entity.functions.js +9 -1
  117. package/dist/turn/turn-event.entity.js +76 -3
  118. package/dist/turn/turn-event.entity.test.d.ts +1 -0
  119. package/dist/turn/turn-lifecycle.d.ts +46 -8
  120. package/dist/turn/turn-lifecycle.js +33 -3
  121. package/dist/turn/turn-lifecycle.test.d.ts +1 -0
  122. package/dist/turn/turn-run.d.ts +15 -3
  123. package/dist/turn/turn-run.js +37 -9
  124. package/dist/turn/turn.adapter.d.ts +6 -5
  125. package/dist/turn/turn.adapter.js +15 -3
  126. package/dist/turn/turn.conformance.d.ts +11 -2
  127. package/dist/turn/turn.conformance.js +63 -2
  128. package/dist/turn/user-message.entity.d.ts +14 -4
  129. package/dist/turn/user-message.entity.functions.d.ts +21 -0
  130. package/dist/turn/user-message.entity.functions.js +24 -0
  131. package/dist/turn/user-message.entity.js +6 -8
  132. package/dist/turn/user-message.entity.test.d.ts +1 -0
  133. package/dist/util/glob.d.ts +30 -0
  134. package/dist/util/glob.js +91 -0
  135. package/dist/util/glob.test.d.ts +1 -0
  136. package/dist/workspace/bash.conformance.d.ts +37 -0
  137. package/dist/workspace/bash.conformance.js +120 -0
  138. package/dist/workspace/bash.conformance.test.d.ts +1 -0
  139. package/dist/workspace/file-system.conformance.d.ts +38 -0
  140. package/dist/workspace/file-system.conformance.js +254 -0
  141. package/dist/workspace/file-system.conformance.test.d.ts +1 -0
  142. package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
  143. package/dist/workspace/file-system.memory.adapter.js +132 -0
  144. package/package.json +1 -1
  145. package/dist/harness/skill-files.d.ts +0 -17
  146. package/dist/harness/skill-files.js +0 -25
  147. package/dist/ports/shell.port.d.ts +0 -32
  148. package/dist/ports/shell.port.js +0 -16
@@ -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
- /** Input tokens one request may use (system, tool schemas and messages). */
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 by default. */
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
- /** This turn's services: the model, per-turn budgets, data ports… */
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<TurnOutcome, HarnessError | E, Exclude<R, A | TurnServices | Scope.Scope>>;
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
+ });
@@ -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
- /** Closes a turn. Not model-visible: what later turns remember is `turn.reply`. */
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
- * Kinds only Efferent writes: a host record under one of these names is refused.
50
- * `request.prepared` holds each request's full system text and tool
51
- * declarations (and step context in "system" mode), next to the memory
52
- * events: a host that redacts memory, or exports or streams session events,
53
- * handles it the same way.
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
- /** One stored event → the memory entry it is, if it is one. */
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 { TransientTurnEvents, TurnEvent } from "../turn/turn-event.entity.js";
5
- import { MemoryKindOf, TurnEndedData, TurnStartedData } from "./session-event.entity.js";
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.flatMap((started) => decodeEntry({
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", prompt: started.userMessage.text }
23
- })), Effect.map(Option.some));
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 recordedElsewhere = new Set(["turn.started", "turn.ended", "skills.activated"]), withoutPayload = (data) => Object.fromEntries(Object.entries(data).filter(([key]) => key !== "result" && key !== "encoded" && key !== "input"));
31
- export const draftOfTurnEvent = (turn, event) => {
32
- if (TransientTurnEvents.includes(event._tag) || recordedElsewhere.has(event._tag))
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
- if (_tag === "assistant.message") {
38
- const { step, model, usage } = data;
39
- return Option.some({ kind: "step.usage", turn: Option.some(turn), data: { step, model, usage } });
40
- }
41
- return Option.some({ kind: _tag, turn: Option.some(turn), data: _tag === "tool.completed" ? withoutPayload(data) : data });
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"], RESERVED_KINDS = [...MEMORY_KINDS, ...TURN_EVENT_KINDS, ...SESSION_KINDS];
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, reads by position and kind, listing order,
9
- * children and their removal. Every check uses fresh ids and a fresh owner,
10
- * so the kit is safe on a shared database.
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 { ConformanceFailure } from "../conformance.entity.js";
3
- import { ConversationId } from "../domain/message.entity.js";
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 fail = (check, message) => Effect.fail(new ConformanceFailure({ check, message })), expect = (check, holds, message) => holds ? Effect.void : fail(check, message), freshId = Effect.sync(() => ConversationId.make(crypto.randomUUID())), headerOf = (id, owner, parent = Option.none()) => ({
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);