@xandreed/sdk 0.14.0 → 0.15.1

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/agent.adapter.d.ts +66 -34
  3. package/dist/agent.adapter.js +48 -37
  4. package/dist/conformance/index.d.ts +2 -0
  5. package/dist/conformance/index.js +1 -0
  6. package/dist/inbox-runner.adapter.d.ts +18 -0
  7. package/dist/inbox-runner.adapter.js +8 -0
  8. package/dist/index.d.ts +35 -2
  9. package/dist/index.js +59 -2
  10. package/dist/task-executor.adapter.d.ts +32 -0
  11. package/dist/task-executor.adapter.js +10 -0
  12. package/dist/task-inbox-runner.adapter.d.ts +11 -0
  13. package/dist/task-inbox-runner.adapter.js +22 -0
  14. package/dist/turn/memory-checkpoint.adapter.d.ts +17 -0
  15. package/dist/turn/memory-checkpoint.adapter.js +62 -0
  16. package/dist/turn/model-call-recorder.adapter.d.ts +9 -0
  17. package/dist/turn/model-call-recorder.adapter.js +93 -0
  18. package/dist/turn/model-call-recorder.entity.d.ts +145 -0
  19. package/dist/turn/model-call-recorder.entity.functions.d.ts +5 -0
  20. package/dist/turn/model-call-recorder.entity.functions.js +4 -0
  21. package/dist/turn/model-call-recorder.entity.js +8 -0
  22. package/dist/turn/tool-registry.conformance.d.ts +61 -0
  23. package/dist/turn/tool-registry.conformance.js +547 -0
  24. package/dist/turn/turn-after-reply.entity.functions.d.ts +9 -0
  25. package/dist/turn/turn-after-reply.entity.functions.js +4 -0
  26. package/dist/turn/turn-lifecycle.usecase.functions.d.ts +135 -0
  27. package/dist/turn/turn-lifecycle.usecase.functions.js +131 -0
  28. package/dist/turn/turn-observation.entity.d.ts +28 -0
  29. package/dist/turn/turn-observation.entity.functions.d.ts +15 -0
  30. package/dist/turn/turn-observation.entity.functions.js +17 -0
  31. package/dist/turn/turn-observation.entity.js +12 -0
  32. package/dist/turn/turn-overflow.entity.d.ts +7 -0
  33. package/dist/turn/turn-overflow.entity.functions.d.ts +11 -0
  34. package/dist/turn/turn-overflow.entity.functions.js +12 -0
  35. package/dist/turn/turn-overflow.entity.js +5 -0
  36. package/dist/turn/turn-phase.entity.d.ts +462 -0
  37. package/dist/turn/turn-phase.entity.functions.d.ts +44 -0
  38. package/dist/turn/turn-phase.entity.functions.js +49 -0
  39. package/dist/turn/turn-phase.entity.js +13 -0
  40. package/dist/turn/turn-run.usecase.functions.d.ts +72 -0
  41. package/dist/turn/turn-run.usecase.functions.js +329 -0
  42. package/dist/turn/turn.adapter.d.ts +26 -0
  43. package/dist/turn/turn.adapter.js +225 -0
  44. package/package.json +10 -4
  45. package/dist/activity.test.d.ts +0 -1
  46. package/dist/agent.adapter.test.d.ts +0 -1
  47. package/dist/agent.backends.test.d.ts +0 -1
  48. package/dist/agent.golden.test.d.ts +0 -231
  49. package/dist/agent.trace.test.d.ts +0 -1
  50. package/dist/events-guide.test.d.ts +0 -207
  51. package/dist/journal-compat.test.d.ts +0 -1
  52. package/dist/memory.conformance.test.d.ts +0 -1
  53. package/dist/models-plugin.test.d.ts +0 -1
  54. package/dist/plugin-stack.test.d.ts +0 -1
  55. package/dist/resume.journey.test.d.ts +0 -1
  56. package/dist/testing.port.d.ts +0 -16
  57. package/dist/testing.port.js +0 -7
  58. package/dist/turn-composition.test.d.ts +0 -1
@@ -0,0 +1,329 @@
1
+ import { Context, Deferred, Effect, Exit, Option, Ref, Schema } from "effect";
2
+ import { LanguageModel } from "effect/ai";
3
+ import {
4
+ AcceptedModelInput as AcceptedInputSchema,
5
+ assistantUsage,
6
+ canonicalJson,
7
+ CurrentAgentRun,
8
+ CurrentAgentStep,
9
+ CurrentModelCallPolicy,
10
+ CurrentPromptCacheKey,
11
+ CurrentTraceContent,
12
+ fingerprintOf,
13
+ formatModelSelection,
14
+ freezeModelRequest,
15
+ HarnessError,
16
+ LoopProtection,
17
+ harnessDefectsAsFailures,
18
+ isBudgetTokens,
19
+ ModelCatalog,
20
+ ModelOverflowEvidence,
21
+ modelRequestDescriptorOf,
22
+ parseModelSelection,
23
+ replayableContext,
24
+ resolveModelRequest,
25
+ scaledTokens,
26
+ StepLoop,
27
+ TurnEvents,
28
+ TurnMemory,
29
+ TurnPrompt,
30
+ TurnTasks,
31
+ TurnToolbox,
32
+ zeroUsage
33
+ } from "@xandreed/core";
34
+ import {
35
+ applyModelRequestHook,
36
+ checkModelRequest,
37
+ CurrentToolResultReceipt,
38
+ entriesSendingMedia,
39
+ estimateTokens,
40
+ markVolatileContext,
41
+ modelRequestHeaderOf,
42
+ modelRequestTools,
43
+ reconstructModelRequest,
44
+ toolParametersSchema
45
+ } from "@xandreed/core/internal";
46
+ import { TurnObservation } from "./turn-observation.entity.js";
47
+ import { matchingObservation, observeAcceptedInput } from "./turn-observation.entity.functions.js";
48
+ import { tighterOverflowBudget, windowBound } from "./turn-overflow.entity.functions.js";
49
+ const MEDIA_REJECTION_CHARS = 500, defaultLimits = { maxSteps: 50, toolConcurrency: 1, streaming: !0, requireCompletion: !1 }, defaultBudgetTokens = 64000, incomplete = { complete: !1, awaiting: [], facts: {} }, noDirective = { context: Option.none(), toolChoice: Option.none() }, normalVerdict = (verdict) => ({
50
+ ...verdict,
51
+ awaiting: verdict.awaiting.map((tag) => tag.trim()).filter((tag) => tag.length > 0)
52
+ }), failure = (code, message) => new HarnessError({ code, message });
53
+ export const schemaTokens = (tools, active) => active.reduce((sum, name) => {
54
+ const tool = tools.toolkit.tools[name];
55
+ return tool === void 0 ? sum : sum + estimateTokens(`${tool.description ?? ""}${canonicalJson(toolParametersSchema(tool))}`);
56
+ }, 0), cacheKeyOf = (prefix, conversation) => Option.map(Option.filter(Option.some(prefix), (value) => value.length > 0), (value) => `${value}:${conversation}`), stepRequestOf = Effect.fnUntraced(function* (policy, options = {}) {
57
+ const runServices = Context.omit(CurrentToolResultReceipt)(replayableContext(yield* Effect.context())), memory = yield* TurnMemory, prompt = yield* TurnPrompt, events = yield* TurnEvents, tasks = yield* TurnTasks, tools = yield* (yield* TurnToolbox).tools, session = memory.session, run = yield* memory.startRun, inRun = (effect, step = Option.none()) => effect.pipe(Effect.provideService(CurrentAgentRun, Option.some(run)), Effect.provideService(CurrentAgentStep, step), Effect.provide(runServices)), defaultModel = Context.getOption(runServices, LanguageModel.LanguageModel), limits = { ...defaultLimits, ...options.limits, ...policy.limits };
58
+ yield* Schema.decodeUnknownEffect(LoopProtection)(limits).pipe(Effect.mapError((cause) => new HarnessError({ code: "turn.limits", message: "the run's limits carry a degenerate policy, malformedLimit or invalidCalls that does not decode", cause })));
59
+ const budget = policy.budgetTokens ?? options.budgetTokens ?? defaultBudgetTokens;
60
+ if (!isBudgetTokens(budget))
61
+ return yield* Effect.fail(failure("turn.budget", `budgetTokens must be an integer of at least 1, not ${budget}`));
62
+ const stepContext = policy.stepContext ?? "tail", cacheKey = options.cacheKey ?? Option.none(), callPolicy = yield* Effect.service(CurrentModelCallPolicy);
63
+ yield* Option.match(Option.fromNullishOr(policy.initial), {
64
+ onNone: () => Effect.void,
65
+ onSome: (batch) => batch.skills.length === 0 ? Effect.void : tools.activate(batch.skills, "host").pipe(Effect.asVoid)
66
+ });
67
+ yield* inRun(prompt.turnSections);
68
+ const renderScope = { run: run.ordinal, stepContext: stepContext === "tail" ? "tail" : "none" }, before = yield* memory.acceptedBefore;
69
+ yield* inRun(session.maintain({
70
+ phase: "turn-start",
71
+ lastUsage: Option.none(),
72
+ budgetTokens: budget,
73
+ views: tools.views,
74
+ render: renderScope,
75
+ ...Option.match(before, { onNone: () => ({}), onSome: (observed) => ({ observed }) })
76
+ }));
77
+ const planned = yield* Ref.make(Option.none()), replanning = yield* Ref.make(Option.none()), privatePlans = yield* Ref.make(new Map), dispatched = yield* Ref.make(new Map), recovery = yield* Ref.make(Option.none()), recoveryClaims = yield* Ref.make(new Map), inputCeiling = yield* Ref.make(budget), observed = yield* Ref.make({ observed: Option.none(), usage: Option.none(), baseReservedTokens: Option.none() }), windows = yield* Ref.make(new Map), windowBoundOf = (identity) => Option.match(identity, {
78
+ onNone: () => Effect.succeed(Option.none()),
79
+ onSome: (selection) => Effect.gen(function* () {
80
+ const key = formatModelSelection(selection), known = (yield* Ref.get(windows)).get(key);
81
+ if (known !== void 0)
82
+ return known;
83
+ const catalog = yield* Option.match(Context.getOption(runServices, ModelCatalog), { onNone: () => Effect.succeed([]), onSome: (service) => service.list }), bound = windowBound(Option.fromNullishOr(catalog.find((entry) => entry.selection === key)?.limits), callPolicy);
84
+ yield* Ref.update(windows, (all) => new Map([...all, [key, bound]]));
85
+ return bound;
86
+ })
87
+ }), recoveryOutcome = Effect.fnUntraced(function* (done) {
88
+ if (!(yield* Deferred.isDone(done)))
89
+ return !1;
90
+ const terminal = yield* Deferred.await(done).pipe(Effect.interruptible, Effect.exit);
91
+ return Exit.isFailure(terminal) ? yield* terminal : !1;
92
+ }), decide = (info) => Effect.gen(function* () {
93
+ const choice = policy.model === void 0 ? Option.none() : yield* inRun(policy.model(info), Option.some(info.stepIndex)), baseSystem = yield* inRun(prompt.system(Option.flatMap(choice, (value) => value.variant)), Option.some(info.stepIndex)), baseDirective = policy.step === void 0 ? noDirective : yield* inRun(policy.step(info), Option.some(info.stepIndex)), prepared = yield* inRun(applyModelRequestHook({
94
+ run,
95
+ step: info.stepIndex,
96
+ activeTools: info.activeTools,
97
+ system: baseSystem,
98
+ context: baseDirective.context,
99
+ toolChoice: baseDirective.toolChoice
100
+ }), Option.some(info.stepIndex)), directive = { context: prepared.context, toolChoice: prepared.toolChoice };
101
+ yield* Option.match(directive.context, {
102
+ onNone: () => Effect.void,
103
+ onSome: (text) => session.record([{ _tag: "StepContext", step: info.stepIndex, run: run.ordinal, text }], info.stepIndex).pipe(Effect.asVoid)
104
+ });
105
+ return { choice, system: prepared.system, directive, hook: prepared.hook };
106
+ }).pipe(Effect.provideService(CurrentAgentRun, Option.some(run)), Effect.provideService(CurrentAgentStep, Option.some(info.stepIndex))), plan = (info) => Effect.gen(function* () {
107
+ const ready = Option.filter(yield* Ref.get(recovery), (candidate) => candidate.step === info.stepIndex);
108
+ if (Option.isSome(ready)) {
109
+ yield* Ref.set(recovery, Option.none());
110
+ yield* Ref.set(planned, ready);
111
+ return ready.value.outgoing;
112
+ }
113
+ const runCeiling = yield* Ref.get(inputCeiling), again = Option.filter(yield* Ref.getAndSet(replanning, Option.none()), (last) => last.step === info.stepIndex), { choice, system, directive, hook } = Option.isSome(again) ? again.value.decision : yield* decide(info), stepText = stepContext === "system" ? Option.getOrElse(directive.context, () => "") : "", finalSystem = [system, stepText].filter((part) => part.length > 0).join(`
114
+
115
+ `), toolTokens = schemaTokens(tools, info.activeTools), reserved = estimateTokens(finalSystem) + toolTokens, baseReserved = estimateTokens(system) + toolTokens, noRoom = (ceiling) => failure("context.budget", `The system prompt and tool schemas take ${reserved} tokens, all of the ${ceiling}-token context budget: raise budgetTokens or offer fewer tools`);
116
+ if (runCeiling <= reserved)
117
+ return yield* Effect.fail(noRoom(runCeiling));
118
+ const model = Option.isSome(again) ? again.value.model : yield* Option.match(Option.orElse(Option.map(choice, (value) => value.model), () => defaultModel), {
119
+ onNone: () => Effect.succeed(Option.none()),
120
+ onSome: (chosen) => resolveModelRequest(chosen).pipe(Effect.map(Option.some))
121
+ }), descriptor = yield* Option.match(model, { onNone: () => Effect.succeed(Option.none()), onSome: modelRequestDescriptorOf }), identity = Option.flatMap(descriptor, (value) => parseModelSelection(`${value.provider}:${value.model}`)), ceiling = Option.match(yield* windowBoundOf(identity), { onNone: () => runCeiling, onSome: (bound) => Math.min(runCeiling, bound) });
122
+ if (ceiling <= reserved)
123
+ return yield* Effect.fail(noRoom(ceiling));
124
+ const prior = matchingObservation(identity, (yield* Ref.get(observed)).observed);
125
+ yield* session.maintain({
126
+ phase: "step",
127
+ lastUsage: info.lastUsage,
128
+ budgetTokens: ceiling - reserved,
129
+ views: tools.views,
130
+ render: renderScope,
131
+ ...Option.match(prior, { onNone: () => ({}), onSome: (observed) => ({ observed }) })
132
+ });
133
+ const built = yield* session.build({ stepContext: stepContext === "tail" ? "tail" : "none", run: run.ordinal }), render = yield* session.renderRecipe(stepContext === "tail" ? "tail" : "none", run.ordinal), header = {
134
+ version: 1,
135
+ runId: run.id,
136
+ run: run.ordinal,
137
+ step: info.stepIndex,
138
+ strategyVersion: session.strategy.version,
139
+ system: finalSystem,
140
+ render,
141
+ through: built.through,
142
+ contextFingerprint: built.fingerprint,
143
+ stableMessages: built.stableMessages,
144
+ tools: modelRequestTools(info.activeTools.flatMap((name) => tools.toolkit.tools[name] === void 0 ? [] : [tools.toolkit.tools[name]])),
145
+ toolChoice: Option.getOrElse(directive.toolChoice, () => "auto"),
146
+ model: descriptor,
147
+ cacheKey,
148
+ callPolicy,
149
+ ...Option.match(hook, { onNone: () => ({}), onSome: (hook) => ({ hook }) })
150
+ }, outgoing = { model, system: finalSystem, messages: markVolatileContext(built.messages, built.stableMessages), toolChoice: directive.toolChoice }, frozen = {
151
+ run,
152
+ step: info.stepIndex,
153
+ decision: { choice, system, directive, hook },
154
+ model,
155
+ identity,
156
+ through: built.through,
157
+ render,
158
+ fingerprint: built.fingerprint,
159
+ estimatedTokens: built.estimatedTokens,
160
+ reservedTokens: reserved,
161
+ baseReservedTokens: baseReserved,
162
+ header,
163
+ outgoing
164
+ };
165
+ yield* events.publish({
166
+ _tag: "context.built",
167
+ step: info.stepIndex,
168
+ turn: run.turn,
169
+ run: run.ordinal,
170
+ strategy: session.strategy.id,
171
+ strategyVersion: session.strategy.version,
172
+ fingerprint: built.fingerprint,
173
+ systemFingerprint: fingerprintOf(system),
174
+ estimatedTokens: built.estimatedTokens,
175
+ reservedTokens: reserved,
176
+ budgetTokens: ceiling,
177
+ compactions: built.compactions.length,
178
+ activeTools: info.activeTools
179
+ });
180
+ yield* memory.prepareRequest(header);
181
+ yield* Ref.update(privatePlans, (all) => new Map([...all, [canonicalJson(header), frozen]]));
182
+ yield* Ref.set(planned, Option.some(frozen));
183
+ return outgoing;
184
+ }).pipe(Effect.provideService(CurrentAgentRun, Option.some(run)), Effect.provideService(CurrentAgentStep, Option.some(info.stepIndex))), request = {
185
+ run,
186
+ tools,
187
+ handlers: Context.merge(runServices, tools.handlers),
188
+ limits,
189
+ initial: Option.fromNullishOr(policy.initial),
190
+ plan,
191
+ dispatch: Effect.fnUntraced(function* (step, actual) {
192
+ const stored = yield* memory.requestSnapshot, header = yield* modelRequestHeaderOf(stored.requestEvents ?? stored.requests, run.id, step, run.ordinal), expected = yield* reconstructModelRequest(header, stored.entries);
193
+ yield* checkModelRequest(expected, {
194
+ prompt: actual.prompt,
195
+ tools: modelRequestTools(actual.tools),
196
+ toolChoice: actual.toolChoice,
197
+ model: yield* modelRequestDescriptorOf(actual.model),
198
+ cacheKey: yield* Effect.service(CurrentPromptCacheKey),
199
+ callPolicy: yield* Effect.service(CurrentModelCallPolicy)
200
+ });
201
+ const frozen = (yield* Ref.get(privatePlans)).get(canonicalJson(header));
202
+ yield* Ref.update(dispatched, (all) => {
203
+ const next = new Map(all);
204
+ if (frozen === void 0)
205
+ next.delete(step);
206
+ else
207
+ next.set(step, { ...frozen, header });
208
+ return next;
209
+ });
210
+ }),
211
+ record: (step, tail, accepted) => Effect.gen(function* () {
212
+ const sent = Option.fromNullishOr((yield* Ref.get(dispatched)).get(step)), prepared = Option.fromNullishOr([...(yield* Ref.get(privatePlans)).values()].findLast((plan) => plan.step === step)), frozen = Option.filter(Option.orElse(sent, () => prepared), (plan) => plan.run.id === run.id && plan.run.ordinal === run.ordinal), usage = Option.flatMap(Option.fromNullishOr(tail.find((message) => message.role === "assistant")), assistantUsage), candidate = accepted === void 0 ? Option.none() : yield* Schema.encodeEffect(Schema.fromJsonString(AcceptedInputSchema))(accepted).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(AcceptedInputSchema))), Effect.map((fact) => ({
213
+ observed: Option.flatMap(frozen, (plan) => observeAcceptedInput({ estimatedTokens: plan.estimatedTokens, reservedTokens: plan.reservedTokens }, fact)),
214
+ usage,
215
+ baseReservedTokens: Option.map(frozen, (plan) => plan.baseReservedTokens)
216
+ })), Effect.flatMap((state) => Schema.encodeEffect(Schema.fromJsonString(TurnObservation))(state)), Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(TurnObservation))), Effect.map(Option.some), Effect.mapError((cause) => new HarnessError({ code: "request.diverged", message: "accepted model input did not encode", cause }))), recorded = yield* session.recordTail(tail, tools.views, step, run.ordinal);
217
+ yield* Option.match(candidate, { onNone: () => Effect.void, onSome: (state) => Ref.set(observed, state).pipe(Effect.andThen(memory.deferAfterReply(afterRun))) });
218
+ return recorded;
219
+ }).pipe(Effect.provideService(CurrentAgentRun, Option.some(run)), Effect.provideService(CurrentAgentStep, Option.some(step))),
220
+ onContextOverflow: Effect.fnUntraced(function* (step, evidence) {
221
+ const known = (yield* Ref.get(recoveryClaims)).get(step);
222
+ if (known !== void 0)
223
+ return yield* recoveryOutcome(known);
224
+ const failed = (yield* Ref.get(dispatched)).get(step);
225
+ if (failed === void 0)
226
+ return !1;
227
+ return yield* Effect.uninterruptible(Effect.gen(function* () {
228
+ const fresh = yield* Deferred.make(), previous = yield* Ref.modify(recoveryClaims, (all) => [Option.fromNullishOr(all.get(step)), all.has(step) ? all : new Map([...all, [step, fresh]])]);
229
+ if (Option.isSome(previous))
230
+ return yield* recoveryOutcome(previous.value);
231
+ const terminal = yield* inRun(harnessDefectsAsFailures(Effect.gen(function* () {
232
+ const facts = yield* Option.match(Option.fromNullishOr(evidence), {
233
+ onNone: () => Effect.succeed(Option.none()),
234
+ onSome: (fact) => Schema.encodeEffect(Schema.fromJsonString(ModelOverflowEvidence))(fact).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(ModelOverflowEvidence))), Effect.map(Option.some), Effect.mapError((cause) => new HarnessError({ code: "request.diverged", message: "overflow evidence did not encode", cause })))
235
+ });
236
+ yield* memory.requestSnapshot;
237
+ const identity = Option.orElse(Option.map(facts, (fact) => fact.model), () => failed.identity), catalog = yield* Option.match(Context.getOption(runServices, ModelCatalog), { onNone: () => Effect.succeed([]), onSome: (service) => service.list }), limits = Option.flatMap(identity, (selection) => Option.fromNullishOr(catalog.find((entry) => entry.selection === formatModelSelection(selection))?.limits)), contextWindow = Option.orElse(Option.flatMap(facts, (fact) => Option.fromNullishOr(fact.limits.contextWindow)), () => Option.flatMap(limits, (value) => Option.fromNullishOr(value.contextWindow))), output = Option.flatMap(facts, (fact) => Option.fromNullishOr(fact.limits.maxOutputTokens)), bounds = {
238
+ ...Option.match(contextWindow, { onNone: () => ({}), onSome: (contextWindow) => ({ contextWindow }) }),
239
+ ...Option.match(output, { onNone: () => ({}), onSome: (maxOutputTokens) => ({ maxOutputTokens }) })
240
+ }, sameObservation = freezeModelRequest(matchingObservation(identity, (yield* Ref.get(observed)).observed)), failedComparable = scaledTokens(failed.estimatedTokens, sameObservation), tighter = tighterOverflowBudget(yield* Ref.get(inputCeiling), failed.reservedTokens, failedComparable, bounds);
241
+ if (Option.isNone(tighter))
242
+ return !1;
243
+ yield* Ref.update(inputCeiling, (previous) => Math.min(previous, tighter.value.inputCeiling));
244
+ yield* session.maintain({
245
+ phase: "overflow",
246
+ lastUsage: (yield* Ref.get(observed)).usage,
247
+ budgetTokens: tighter.value.messageBudget,
248
+ views: tools.views,
249
+ render: { run: run.ordinal, stepContext: failed.render.stepContext },
250
+ ...Option.match(sameObservation, { onNone: () => ({}), onSome: (observed) => ({ observed }) })
251
+ });
252
+ const built = yield* session.build({ run: run.ordinal, stepContext: failed.render.stepContext }), comparable = scaledTokens(built.estimatedTokens, sameObservation), ceiling = yield* Ref.get(inputCeiling);
253
+ if (comparable >= failedComparable || comparable > Math.min(tighter.value.messageBudget, ceiling - failed.reservedTokens))
254
+ return !1;
255
+ const render = yield* session.renderRecipe(failed.render.stepContext, run.ordinal), header = { ...failed.header, render, through: built.through, contextFingerprint: built.fingerprint, stableMessages: built.stableMessages }, outgoing = { ...failed.outgoing, messages: freezeModelRequest(markVolatileContext(built.messages, built.stableMessages)) }, candidate = { ...failed, through: built.through, render, fingerprint: built.fingerprint, estimatedTokens: built.estimatedTokens, header, outgoing };
256
+ yield* events.publish({
257
+ _tag: "context.built",
258
+ step,
259
+ turn: run.turn,
260
+ run: run.ordinal,
261
+ strategy: session.strategy.id,
262
+ strategyVersion: session.strategy.version,
263
+ fingerprint: built.fingerprint,
264
+ systemFingerprint: fingerprintOf(failed.decision.system),
265
+ estimatedTokens: built.estimatedTokens,
266
+ reservedTokens: failed.reservedTokens,
267
+ budgetTokens: ceiling,
268
+ compactions: built.compactions.length,
269
+ activeTools: failed.header.tools.map((tool) => tool.name)
270
+ });
271
+ yield* memory.prepareRequest(header);
272
+ yield* Ref.update(privatePlans, (all) => new Map([...all, [canonicalJson(header), candidate]]));
273
+ yield* Ref.set(planned, Option.some(candidate));
274
+ yield* Ref.set(recovery, Option.some(candidate));
275
+ return !0;
276
+ })), Option.some(step)).pipe(Effect.interruptible, Effect.exit);
277
+ yield* Deferred.done(fresh, terminal);
278
+ return yield* terminal;
279
+ }));
280
+ }),
281
+ pendingSteers: memory.pendingSteers,
282
+ steer: (step) => memory.steer(step, run.ordinal),
283
+ recorded: (step) => memory.requestSnapshot.pipe(Effect.map(({ entries }) => entries.filter((entry) => entry.runId === run.id && entry.turn === run.turn && entry.step === step && (entry.body._tag === "Message" || entry.body._tag === "ToolResult") && entry.body.run === run.ordinal))),
284
+ onMediaRejected: Effect.fnUntraced(function* (reason) {
285
+ const last = yield* Ref.get(planned);
286
+ if (Option.isNone(last))
287
+ return !1;
288
+ const entries = yield* session.entries, through = last.value.through, built = Option.isNone(through) ? [] : entries.slice(0, entries.findIndex((entry) => entry.id === through.value) + 1), sent = entriesSendingMedia(built, last.value.render);
289
+ if (sent.length === 0)
290
+ return !1;
291
+ yield* session.record([{
292
+ _tag: "Compaction",
293
+ strategy: session.strategy.id,
294
+ version: session.strategy.version,
295
+ action: { _tag: "OmitMedia", entries: sent, reason: reason.slice(0, MEDIA_REJECTION_CHARS) }
296
+ }], last.value.step);
297
+ yield* Ref.set(replanning, last);
298
+ return !0;
299
+ }),
300
+ completion: (info) => policy.completion === void 0 ? Effect.succeed(incomplete) : inRun(policy.completion(info), Option.some(info.stepIndex)).pipe(Effect.map(normalVerdict)),
301
+ steering: options.steering ?? Effect.succeed(Option.none()),
302
+ ...options.waiting === void 0 ? {} : { waiting: inRun(options.waiting) },
303
+ correctives: Option.fromNullishOr(policy.correctives),
304
+ events,
305
+ tasks,
306
+ cacheKey,
307
+ callPolicy,
308
+ captureTraceContent: policy.captureTraceContent ?? options.captureTraceContent ?? (yield* Effect.service(CurrentTraceContent))
309
+ }, maintenance = inRun(harnessDefectsAsFailures(Effect.gen(function* () {
310
+ yield* memory.requestSnapshot;
311
+ const last = yield* Ref.get(planned), accepted = yield* Ref.get(observed), reserve = Option.orElse(accepted.baseReservedTokens, () => Option.map(last, (plan) => plan.baseReservedTokens)), remaining = (yield* Ref.get(inputCeiling)) - Option.getOrElse(reserve, () => 0);
312
+ yield* session.maintain({
313
+ phase: "turn-end",
314
+ lastUsage: accepted.usage,
315
+ budgetTokens: Math.max(1, remaining),
316
+ views: tools.views,
317
+ render: { run: run.ordinal, stepContext: "none" },
318
+ ...Option.match(accepted.observed, { onNone: () => ({}), onSome: (observed) => ({ observed }) })
319
+ });
320
+ }))), terminal = yield* Ref.make(Option.none()), afterRun = Effect.uninterruptible(Effect.gen(function* () {
321
+ const fresh = yield* Deferred.make(), previous = yield* Ref.modify(terminal, (existing) => [existing, Option.orElse(existing, () => Option.some(fresh))]);
322
+ if (Option.isSome(previous))
323
+ return yield* Deferred.await(previous.value).pipe(Effect.interruptible);
324
+ const exit = yield* maintenance.pipe(Effect.interruptible, Effect.exit);
325
+ yield* Deferred.done(fresh, exit);
326
+ return yield* exit;
327
+ }));
328
+ return request;
329
+ }), runTurnLoop = (policy, options = {}) => (options.waiting ?? Effect.succeed(!1)).pipe(Effect.flatMap((waiting) => waiting ? Effect.succeed({ outcome: "waiting", reason: "waiting", text: "", steps: 0, usage: zeroUsage }) : stepRequestOf(policy, options).pipe(Effect.flatMap((request) => StepLoop.pipe(Effect.flatMap((loop) => loop.run(request)))))));
@@ -0,0 +1,26 @@
1
+ import { Layer } from "effect";
2
+ import { ConversationMemory, RunContext, ToolRegistry, TurnEvents, TurnMemory, TurnPrompt, TurnTasks, TurnToolbox } from "@xandreed/core";
3
+ import type { TurnError, TurnLiveInput } from "@xandreed/core";
4
+ /**
5
+ * One admitted turn's services, over the turn's writer, built in this order:
6
+ * the event bus and the tasks, with the writer as the bus's FIRST
7
+ * subscriber (every stored event is queued before any reaction runs) and
8
+ * the turn's usage (`RunContext.usage`) as the second; then the memory
9
+ * session, opened over the session's earlier memory events where this
10
+ * layer is built (a digester or summarizer there is used), and RunContext.
11
+ * It also holds the tools slot (`TurnToolbox`) and the prompt state
12
+ * (`TurnPrompt`). Where the turn stands is one `TurnPhase`, moved by the
13
+ * pure transitions of `turn-phase.entity.functions.ts`; what a transition
14
+ * plans (a record, a publish) runs after it. The message was stored when
15
+ * the turn began; memory takes it at `TurnMemory.persistMessage`, and
16
+ * records nothing before it (`TurnMemory.context` waits for it). Its
17
+ * reply, and what memory records after it, the writer holds for the turn's
18
+ * closing commit (`TurnWriter.closing`), stored with `turn.ended`; a failed
19
+ * reply after a completed one holds it again, failed, under its own id.
20
+ * Tasks run in the layer's scope: they are interrupted when it closes.
21
+ * `TurnMemory.persistReply` runs under its span, `turn.persist_reply`
22
+ * (inline `turn.ended` subscribers' spans nest under it).
23
+ *
24
+ * @public
25
+ */
26
+ export declare const TurnLive: (input: TurnLiveInput) => Layer.Layer<RunContext | TurnEvents | TurnTasks | TurnMemory | TurnToolbox | TurnPrompt, TurnError, ConversationMemory | ToolRegistry>;
@@ -0,0 +1,225 @@
1
+ import { Context, Effect, Layer, Match, Option, Ref, Result, Schema, Scope, Semaphore } from "effect";
2
+ import {
3
+ addUsage,
4
+ canonicalJson,
5
+ Capabilities,
6
+ ConversationMemory,
7
+ CurrentAgentRun,
8
+ CurrentAgentStep,
9
+ CurrentModelCallRecorder,
10
+ entriesOfEvents,
11
+ fingerprintOf,
12
+ frameworkDraft,
13
+ HarnessError,
14
+ makeTurnEvents,
15
+ makeTurnTasks,
16
+ MEMORY_KINDS,
17
+ RequestPartData,
18
+ RequestPartHash,
19
+ readStoredKeys,
20
+ RESERVED_KINDS,
21
+ RunContext,
22
+ SectionId,
23
+ storedJson,
24
+ ToolRegistry,
25
+ TurnEvents,
26
+ TurnMemory,
27
+ TurnPrompt,
28
+ TurnTasks,
29
+ TurnToolbox,
30
+ userRoleMessageOf,
31
+ zeroUsage
32
+ } from "@xandreed/core";
33
+ import { draftOfTurnEvent, draftsOfEntries, encodeRequestPart, prepareRequestParts, readerOf, renderSections } from "@xandreed/core/internal";
34
+ import { ReplyStep } from "./turn-phase.entity.js";
35
+ import { addContext, claim, heldWith, hold, numberOf, release, reply, sectionsDue, startRun, take } from "./turn-phase.entity.functions.js";
36
+ import { makeTurnModelCallRecorder } from "./model-call-recorder.adapter.js";
37
+ import { acceptedInputOf } from "./turn-observation.entity.functions.js";
38
+ import { isWriterCause } from "./turn-after-reply.entity.functions.js";
39
+ import { makeMemoryCheckpoint } from "./memory-checkpoint.adapter.js";
40
+ const failure = (code, message) => new HarnessError({ code, message }), storedDraft = (turn) => (event) => event._tag === "host" && RESERVED_KINDS.includes(event.name) ? Effect.fail(failure("events.reserved", `${event.name} is the framework's kind`)) : draftOfTurnEvent(turn, event).pipe(Effect.mapError((error) => failure("events.encode", `${event._tag} does not encode: ${error.message}`)), Effect.map(Option.map((draft) => frameworkDraft(draft.kind, draft.data)))), contextBody = (entry) => ({ _tag: "TurnContext", sectionId: SectionId.make(entry.id), version: entry.version, text: entry.text });
41
+ export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
42
+ const scope = yield* Effect.scope, memory = yield* ConversationMemory, registry = yield* ToolRegistry, sections = Option.getOrElse(yield* Effect.serviceOption(Capabilities), () => []).flatMap((capability) => capability.promptSections), writer = input.turn, admitted = writer.admitted, events = yield* makeTurnEvents({ maxDepth: input.maxEventDepth ?? 8 }), modelCalls = yield* makeTurnModelCallRecorder(writer, (data) => events.publish({ _tag: "model.called", ...data }));
43
+ yield* Effect.addFinalizer(() => modelCalls.close);
44
+ const tasks = yield* makeTurnTasks(scope);
45
+ yield* events.subscribe(Option.some, (event) => storedDraft(admitted.turn)(event).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (draft) => writer.append([draft]) }))));
46
+ const usage = yield* Ref.make(zeroUsage);
47
+ yield* events.subscribe((event) => event._tag === "assistant.message" ? Option.some(event) : Option.none(), (step) => Ref.update(usage, (total) => addUsage(total, step.usage)).pipe(Effect.andThen(modelCalls.legacy({ model: step.model, usage: step.usage, run: step.run, step: step.step, replayed: !1 }))));
48
+ const phase = yield* Ref.make({ _tag: "Waiting", early: [] }), transition = (decide) => Ref.modify(phase, (current) => Result.match(decide(current), {
49
+ onFailure: (error) => [Result.fail(error), current],
50
+ onSuccess: ([value, next]) => [Result.succeed(value), next]
51
+ })).pipe(Effect.flatMap(Effect.fromResult)), history = yield* Ref.make(Option.none()), rawHistory = yield* Ref.make(Option.none()), decoded = (stored) => entriesOfEvents(stored).pipe(Effect.mapError((error) => failure("memory.log", `undecodable memory events: ${error.message}`))), historicalRows = Ref.get(rawHistory).pipe(Effect.flatMap(Option.match({
52
+ onNone: () => writer.history(MEMORY_KINDS).pipe(Effect.tap((rows) => Ref.set(rawHistory, Option.some(rows)))),
53
+ onSome: Effect.succeed
54
+ }))), readHistory = historicalRows.pipe(Effect.flatMap(decoded)), checkpoint = writer.memorySnapshot === void 0 ? void 0 : yield* makeMemoryCheckpoint(writer, historicalRows), storedEntry = readStoredKeys("turn.started", [writer.started]).pipe(Effect.map((starts) => starts.map((started) => started.keys.entry)), Effect.mapError((error) => failure("memory.log", `undecodable turn.started: ${error.message}`))), draftsOf = (entries) => draftsOfEntries(entries).pipe(Effect.mapError((error) => failure("memory.log", error.message)), Effect.map((drafts) => drafts.map((draft) => frameworkDraft(draft.kind, draft.data)))), holding = yield* Semaphore.make(1), holdNow = (held) => draftsOf(held).pipe(Effect.flatMap(writer.closing), Effect.andThen(Ref.update(phase, hold(held)))), log = {
55
+ read: readHistory.pipe(Effect.tap((entries) => Ref.set(history, Option.some(entries)))),
56
+ ...checkpoint === void 0 ? {} : { checkpoint },
57
+ snapshot: writer.snapshot(MEMORY_KINDS).pipe(Effect.flatMap(decoded), Effect.flatMap((entries) => Ref.get(phase).pipe(Effect.map((current) => Option.isNone(numberOf(current)) ? entries.filter((entry) => entry.body._tag !== "TurnStarted" || entry.runId !== admitted.runId) : entries)))),
58
+ append: Effect.fnUntraced(function* (entries) {
59
+ if (Option.isNone(numberOf(yield* Ref.get(phase))) && entries.some((entry) => entry.body._tag !== "TurnStarted"))
60
+ return yield* Effect.fail(failure("turn.unstarted", "memory records nothing before the turn's message is taken (persistMessage); TurnMemory.context waits for it"));
61
+ const started = entries.filter((entry) => entry.body._tag === "TurnStarted"), stored = started.length === 0 ? [] : yield* storedEntry;
62
+ if (started.some((entry) => entry.turn !== admitted.turn || !stored.includes(entry.id)))
63
+ return yield* Effect.fail(failure("turn.numbering", `memory took the message as ${started.map((entry) => `${entry.id} of turn ${entry.turn}`).join()}, the session stored ${stored.join()} of turn ${admitted.turn}`));
64
+ yield* holding.withPermits(1)(Ref.get(phase).pipe(Effect.flatMap((current) => Option.match(heldWith(entries)(current), {
65
+ onNone: () => draftsOf(entries).pipe(Effect.flatMap(writer.append), Effect.andThen(writer.flush)),
66
+ onSome: holdNow
67
+ }))));
68
+ })
69
+ }, session = yield* memory.open({ conversation: admitted.session.id, runId: admitted.runId, log }).pipe(Effect.provideService(CurrentModelCallRecorder, modelCalls.recorder), Effect.provideService(CurrentAgentRun, Option.none()), Effect.provideService(CurrentAgentStep, Option.none())), reader = readerOf(session), slot = yield* Ref.make({ _tag: "Closed" }), unavailable = Effect.fail(failure("tools.unavailable", "Tools are not open yet")), openTools = Ref.get(slot).pipe(Effect.flatMap((current) => Match.valueTags(current, {
70
+ Closed: () => unavailable,
71
+ Opening: () => unavailable,
72
+ Open: ({ tools }) => Effect.succeed(tools)
73
+ }))), run = RunContext.of({
74
+ conversation: admitted.session.id,
75
+ session: admitted.session,
76
+ runId: admitted.runId,
77
+ userMessage: admitted.userMessage,
78
+ admitted,
79
+ memory: reader,
80
+ events,
81
+ tasks,
82
+ activate: (skills) => openTools.pipe(Effect.flatMap((tools) => tools.activate(skills, "host"))),
83
+ flush: writer.flush,
84
+ write: writer.write,
85
+ usage: Ref.get(usage)
86
+ }), requestKinds = [...MEMORY_KINDS, "request.part", "request.prepared"], snapshots = yield* Semaphore.make(1), snapshot = yield* Ref.make(Option.none()), refreshRequests = Effect.gen(function* () {
87
+ const known = yield* Option.match(yield* Ref.get(snapshot), {
88
+ onNone: () => Effect.gen(function* () {
89
+ const entries = yield* Ref.get(history).pipe(Effect.flatMap(Option.match({ onNone: () => readHistory, onSome: Effect.succeed }))), requestEvents = yield* writer.history(["request.part"]);
90
+ return { entries, requests: [], requestEvents, cursor: writer.started.seq - 1 };
91
+ }),
92
+ onSome: Effect.succeed
93
+ }), stored = (yield* writer.snapshot(requestKinds, known.cursor)).filter((event) => event.session === writer.started.session && event.seq > known.cursor), ownHeaders = (yield* readStoredKeys("request.prepared", stored).pipe(Effect.mapError((error) => failure("request.decode", error.message)))).flatMap((header) => header.keys.runId === admitted.runId ? [header.event] : []), selected = new Set(ownHeaders), next = {
94
+ entries: [...known.entries, ...yield* decoded(stored.filter((event) => MEMORY_KINDS.includes(event.kind)))],
95
+ requests: [...known.requests, ...ownHeaders],
96
+ requestEvents: [...known.requestEvents, ...stored.filter((event) => event.kind === "request.part" || selected.has(event))],
97
+ cursor: stored.reduce((last, event) => Math.max(last, event.seq), known.cursor)
98
+ };
99
+ yield* Ref.set(snapshot, Option.some(next));
100
+ return next;
101
+ }), requestSnapshot = snapshots.withPermits(1)(refreshRequests).pipe(Effect.map(({ entries, requests, requestEvents }) => ({ entries, requests, requestEvents }))), partKey = Schema.Struct({ hash: RequestPartHash }), prepareRequest = Effect.fnUntraced(function* (header) {
102
+ const encoded = yield* prepareRequestParts(header).pipe(Effect.flatMap((prepared) => Effect.all({
103
+ header: storedJson(prepared.header),
104
+ parts: Effect.forEach(prepared.parts, (expected) => storedJson(expected).pipe(Effect.map((data) => ({ expected, data }))), { concurrency: 1 })
105
+ }, { concurrency: 1 })), Effect.mapError((cause) => new HarnessError({ code: "request.encode", message: "request parts did not encode", cause })));
106
+ yield* snapshots.withPermits(1)(Effect.gen(function* () {
107
+ const visible = (yield* refreshRequests).requestEvents.filter((event) => event.kind === "request.part"), keyed = yield* Effect.forEach(visible, (event) => Schema.decodeUnknownEffect(partKey)(event.data).pipe(Effect.map((keys) => ({ keys, event })), Effect.mapError((cause) => new HarnessError({ code: "request.decode", message: "request part lookup keys did not decode", cause }))), { concurrency: 1 }), missing = yield* Effect.forEach(encoded.parts, ({ expected, data }) => Effect.gen(function* () {
108
+ const category = Match.valueTags(expected.part, { System: () => "system", Tool: () => "tools" }), prior = keyed.filter(({ keys }) => keys.hash === expected.hash);
109
+ yield* Effect.forEach(prior, ({ event }) => Schema.decodeUnknownEffect(RequestPartData)(event.data).pipe(Effect.mapError((cause) => new HarnessError({ code: "request.diverged", message: `${category}: stored request part did not decode`, cause })), Effect.flatMap((part) => encodeRequestPart(part.part).pipe(Effect.mapError((cause) => new HarnessError({ code: "request.diverged", message: `${category}: stored request part did not encode`, cause })))), Effect.flatMap((part) => part.hash === expected.hash && canonicalJson(part.part) === canonicalJson(expected.part) ? Effect.void : Effect.fail(new HarnessError({ code: "request.diverged", message: `${category}: stored request part content does not match its hash` })))), { concurrency: 1, discard: !0 });
110
+ return prior.length === 0 ? [frameworkDraft("request.part", data)] : [];
111
+ }), { concurrency: 1 });
112
+ yield* writer.append([...missing.flat(), frameworkDraft("request.prepared", encoded.header)]);
113
+ yield* writer.flush;
114
+ }));
115
+ }), recordContext = (bodies) => bodies.length === 0 ? Effect.void : session.record(bodies, 0).pipe(Effect.asVoid), acceptingSteers = Ref.get(phase).pipe(Effect.flatMap((current) => Match.valueTags(current, {
116
+ Waiting: () => Effect.fail(failure("turn.unstarted", "The user's message is not persisted yet")),
117
+ Taking: () => Effect.fail(failure("turn.unstarted", "The user's message is not persisted yet")),
118
+ Taken: () => Effect.void,
119
+ Replied: () => Effect.fail(failure("turn.closed", "The turn's reply is recorded: no steering is accepted after it"))
120
+ }))), afterReplyWork = yield* Ref.make(Option.none()), turnMemory = TurnMemory.of({
121
+ ...reader,
122
+ strategy: session.strategy,
123
+ session,
124
+ pendingSteers: acceptingSteers.pipe(Effect.andThen(writer.steers), Effect.map((inputs) => inputs.length > 0)),
125
+ steer: Effect.fnUntraced(function* (step, ordinal) {
126
+ yield* acceptingSteers;
127
+ const bodies = (yield* writer.steers).map((input) => ({
128
+ _tag: "Message",
129
+ inbox: input.id,
130
+ run: ordinal,
131
+ message: userRoleMessageOf(input.userMessage)
132
+ })), gateFailure = yield* Ref.make(Option.none()), recorded = yield* session.record(bodies, step, (entries) => holding.withPermits(1)(acceptingSteers.pipe(Effect.result, Effect.flatMap((result) => Result.match(result, {
133
+ onFailure: (error) => Ref.set(gateFailure, Option.some(error)).pipe(Effect.as(!1)),
134
+ onSuccess: () => writer.take(entries)
135
+ })))));
136
+ yield* Option.match(yield* Ref.get(gateFailure), { onNone: () => Effect.void, onSome: Effect.fail });
137
+ return recorded.flatMap((entry) => entry.body._tag === "Message" ? [entry.body.message] : []);
138
+ }),
139
+ prepareRequest,
140
+ requestSnapshot,
141
+ number: Ref.get(phase).pipe(Effect.flatMap((current) => Option.match(numberOf(current), {
142
+ onNone: () => Effect.fail(failure("turn.unstarted", "The user's message is not persisted yet")),
143
+ onSome: (number) => Effect.succeed(number)
144
+ }))),
145
+ startRun: transition(startRun(admitted.runId)),
146
+ deferAfterReply: (work) => Ref.set(afterReplyWork, Option.some(work)),
147
+ afterReply: Effect.gen(function* () {
148
+ const work = yield* Ref.getAndSet(afterReplyWork, Option.none());
149
+ if (Option.isNone(work) || writer.seal === void 0)
150
+ return;
151
+ const sealed = yield* writer.seal;
152
+ if (Option.isNone(sealed))
153
+ return;
154
+ yield* work.value.pipe(Effect.raceFirst(sealed.value), Effect.catchCause((cause) => isWriterCause(cause) ? Effect.failCause(cause) : Effect.logWarning("the work after the turn's reply did not finish; the next run's start maintains what it left", cause)));
155
+ }).pipe(Effect.withSpan("turn.after_reply")),
156
+ acceptedBefore: Effect.gen(function* () {
157
+ const starts = (yield* historicalRows).filter((event) => event.session === admitted.session.id && event.kind === "turn.started"), after = Option.match(Option.fromNullishOr(starts.at(-1)), { onNone: () => writer.started.seq - 1, onSome: (last) => last.seq - 1 });
158
+ return acceptedInputOf(yield* writer.snapshot(["step.usage", "context.built"], after));
159
+ }),
160
+ persistMessage: Effect.gen(function* () {
161
+ yield* transition(claim);
162
+ const number = admitted.turn;
163
+ yield* session.record([{ _tag: "TurnStarted", userMessage: admitted.userMessage }], 0).pipe(Effect.onError(() => Ref.update(phase, release)));
164
+ yield* recordContext(yield* transition(take(number)));
165
+ yield* events.publish({
166
+ _tag: "turn.started",
167
+ runId: admitted.runId,
168
+ turn: number,
169
+ userMessage: admitted.userMessage,
170
+ meta: admitted.meta,
171
+ ...admitted.resumes === void 0 ? {} : { resumes: admitted.resumes }
172
+ });
173
+ return number;
174
+ }),
175
+ context: (entry) => transition(addContext(contextBody(entry))).pipe(Effect.flatMap(recordContext)),
176
+ persistReply: Effect.fn("turn.persist_reply")(function* (outcome) {
177
+ const step = yield* holding.withPermits(1)(transition(reply(outcome.outcome)).pipe(Effect.tap((next) => ReplyStep.guards.Hold(next) ? holdNow(next.held) : Effect.void)));
178
+ if (!ReplyStep.guards.Record(step))
179
+ return;
180
+ yield* session.record([{ _tag: "TurnEnded", outcome: outcome.outcome, reply: outcome.reply }], 0);
181
+ yield* events.publish({ _tag: "turn.ended", runId: admitted.runId, turn: step.number, outcome: outcome.outcome, reply: outcome.reply, usage: yield* Ref.get(usage) });
182
+ })
183
+ }), toolbox = TurnToolbox.of({
184
+ open: Effect.gen(function* () {
185
+ if (!(yield* Ref.modify(slot, (current) => current._tag === "Closed" ? [!0, { _tag: "Opening" }] : [!1, current])))
186
+ return yield* Effect.fail(failure("tools.opened", "The turn's tools are already open"));
187
+ const tools = yield* registry.open(session).pipe(Effect.provideService(RunContext, run), Scope.provide(scope), Effect.onError(() => Ref.set(slot, { _tag: "Closed" })));
188
+ yield* Ref.set(slot, { _tag: "Open", tools });
189
+ return tools;
190
+ }),
191
+ tools: openTools
192
+ }), promptContext = (tools, variant) => tools.active.pipe(Effect.map((active) => ({ variant, active, skills: tools.skills }))), rendered = (tools, variant, tiers) => promptContext(tools, variant).pipe(Effect.flatMap((context) => renderSections(sections.filter(tiers), context))), lastSystem = yield* Ref.make(Option.fromNullishOr((yield* session.entries).flatMap((entry) => entry.body._tag === "SystemPrepared" ? [entry.body.fingerprint] : []).at(-1))), systems = yield* Ref.make(new Map), prompt = TurnPrompt.of({
193
+ system: Effect.fnUntraced(function* (variant) {
194
+ const key = Option.getOrElse(variant, () => ""), known = (yield* Ref.get(systems)).get(key);
195
+ if (known !== void 0)
196
+ return known;
197
+ const parts = yield* rendered(yield* openTools, variant, (section) => section.tier !== "turn"), text = [input.system ?? "", ...parts.map((part) => part.text)].filter((part) => part.trim().length > 0).join(`
198
+
199
+ `), fingerprint = fingerprintOf(text);
200
+ if (!Option.contains(yield* Ref.get(lastSystem), fingerprint)) {
201
+ yield* session.record([{
202
+ _tag: "SystemPrepared",
203
+ fingerprint,
204
+ text,
205
+ sections: parts.map((part) => ({ id: SectionId.make(part.section.id), version: part.section.version, fingerprint: fingerprintOf(part.text) }))
206
+ }], 0);
207
+ yield* Ref.set(lastSystem, Option.some(fingerprint));
208
+ }
209
+ yield* Ref.update(systems, (all) => new Map([...all, [key, text]]));
210
+ return text;
211
+ }),
212
+ turnSections: Effect.gen(function* () {
213
+ if (!(yield* transition(sectionsDue)))
214
+ return;
215
+ const parts = yield* rendered(yield* openTools, Option.none(), (section) => section.tier === "turn");
216
+ yield* parts.length === 0 ? Effect.void : session.record(parts.map(({ section, text }) => ({
217
+ _tag: "TurnContext",
218
+ sectionId: SectionId.make(section.id),
219
+ version: section.version,
220
+ text
221
+ })), 0);
222
+ })
223
+ });
224
+ return Context.make(RunContext, run).pipe(Context.add(CurrentModelCallRecorder, modelCalls.recorder), Context.add(CurrentAgentRun, Option.none()), Context.add(CurrentAgentStep, Option.none()), Context.add(TurnEvents, events), Context.add(TurnTasks, tasks), Context.add(TurnMemory, turnMemory), Context.add(TurnToolbox, toolbox), Context.add(TurnPrompt, prompt));
225
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xandreed/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "engines": {
5
5
  "node": ">=24",
6
6
  "bun": ">=1.3.0"
@@ -11,15 +11,21 @@
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
13
  "default": "./dist/index.js"
14
+ },
15
+ "./conformance": {
16
+ "types": "./dist/conformance/index.d.ts",
17
+ "default": "./dist/conformance/index.js"
14
18
  }
15
19
  },
16
20
  "dependencies": {
17
- "@xandreed/core": "0.14.0",
18
- "effect": "4.0.7",
19
- "@xandreed/runtime": "0.14.0"
21
+ "@xandreed/runtime": "^0.15.1"
20
22
  },
21
23
  "private": false,
22
24
  "types": "./dist/index.d.ts",
25
+ "peerDependencies": {
26
+ "@xandreed/core": "^0.15.1",
27
+ "effect": "^4.0.7"
28
+ },
23
29
  "files": [
24
30
  "dist",
25
31
  "skills",
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};