@xandreed/sdk 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent.adapter.d.ts +111 -11
- package/dist/agent.adapter.js +80 -34
- package/dist/conformance/index.d.ts +2 -0
- package/dist/conformance/index.js +1 -0
- package/dist/inbox-runner.adapter.d.ts +18 -0
- package/dist/inbox-runner.adapter.js +8 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.js +58 -1
- package/dist/task-executor.adapter.d.ts +32 -0
- package/dist/task-executor.adapter.js +10 -0
- package/dist/task-inbox-runner.adapter.d.ts +11 -0
- package/dist/task-inbox-runner.adapter.js +22 -0
- package/dist/turn/memory-checkpoint.adapter.d.ts +17 -0
- package/dist/turn/memory-checkpoint.adapter.js +62 -0
- package/dist/turn/model-call-recorder.adapter.d.ts +9 -0
- package/dist/turn/model-call-recorder.adapter.js +93 -0
- package/dist/turn/model-call-recorder.entity.d.ts +145 -0
- package/dist/turn/model-call-recorder.entity.functions.d.ts +5 -0
- package/dist/turn/model-call-recorder.entity.functions.js +4 -0
- package/dist/turn/model-call-recorder.entity.js +8 -0
- package/dist/turn/tool-registry.conformance.d.ts +61 -0
- package/dist/turn/tool-registry.conformance.js +547 -0
- package/dist/turn/turn-after-reply.entity.functions.d.ts +9 -0
- package/dist/turn/turn-after-reply.entity.functions.js +4 -0
- package/dist/turn/turn-lifecycle.usecase.functions.d.ts +135 -0
- package/dist/turn/turn-lifecycle.usecase.functions.js +131 -0
- package/dist/turn/turn-observation.entity.d.ts +28 -0
- package/dist/turn/turn-observation.entity.functions.d.ts +15 -0
- package/dist/turn/turn-observation.entity.functions.js +17 -0
- package/dist/turn/turn-observation.entity.js +12 -0
- package/dist/turn/turn-overflow.entity.d.ts +7 -0
- package/dist/turn/turn-overflow.entity.functions.d.ts +11 -0
- package/dist/turn/turn-overflow.entity.functions.js +12 -0
- package/dist/turn/turn-overflow.entity.js +5 -0
- package/dist/turn/turn-phase.entity.d.ts +462 -0
- package/dist/turn/turn-phase.entity.functions.d.ts +44 -0
- package/dist/turn/turn-phase.entity.functions.js +49 -0
- package/dist/turn/turn-phase.entity.js +13 -0
- package/dist/turn/turn-run.usecase.functions.d.ts +72 -0
- package/dist/turn/turn-run.usecase.functions.js +329 -0
- package/dist/turn/turn.adapter.d.ts +26 -0
- package/dist/turn/turn.adapter.js +225 -0
- package/package.json +10 -4
- package/dist/agent.adapter.test.d.ts +0 -1
- package/dist/agent.backends.test.d.ts +0 -1
- package/dist/agent.golden.test.d.ts +0 -190
- package/dist/memory.conformance.test.d.ts +0 -1
- package/dist/plugin-stack.test.d.ts +0 -1
- package/dist/testing.port.d.ts +0 -16
- package/dist/testing.port.js +0 -7
- 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 or malformedLimit 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.
|
|
3
|
+
"version": "0.15.0",
|
|
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/
|
|
18
|
-
"effect": "4.0.7",
|
|
19
|
-
"@xandreed/runtime": "0.13.0"
|
|
21
|
+
"@xandreed/runtime": "^0.15.0"
|
|
20
22
|
},
|
|
21
23
|
"private": false,
|
|
22
24
|
"types": "./dist/index.d.ts",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@xandreed/core": "^0.15.0",
|
|
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 {};
|