@xandreed/core 0.4.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.entity.d.ts +15 -0
  3. package/dist/conformance.entity.js +7 -0
  4. package/dist/decision-record.entity.d.ts +23 -23
  5. package/dist/decision-record.entity.functions.d.ts +6 -0
  6. package/dist/decision-record.entity.functions.js +3 -0
  7. package/dist/decision-record.entity.js +4 -4
  8. package/dist/domain/agent-failure.entity.d.ts +6 -6
  9. package/dist/domain/agent-failure.entity.js +12 -1
  10. package/dist/domain/failure.entity.d.ts +2 -2
  11. package/dist/domain/loop-event.entity.d.ts +2 -0
  12. package/dist/domain/message.entity.d.ts +143 -156
  13. package/dist/domain/message.entity.js +6 -6
  14. package/dist/domain/model-call-policy.entity.d.ts +3 -3
  15. package/dist/domain/model-call-policy.entity.js +1 -1
  16. package/dist/domain/model-catalog.entity.d.ts +4 -4
  17. package/dist/domain/model-catalog.entity.js +1 -1
  18. package/dist/domain/model-selection.entity.d.ts +6 -13
  19. package/dist/domain/prompt-provenance.entity.d.ts +6 -6
  20. package/dist/domain/prompt-provenance.entity.js +4 -4
  21. package/dist/domain/token-usage.entity.d.ts +4 -4
  22. package/dist/harness/capability.entity.d.ts +88 -41
  23. package/dist/harness/capability.entity.functions.d.ts +30 -1
  24. package/dist/harness/capability.entity.functions.js +55 -2
  25. package/dist/harness/capability.entity.js +33 -12
  26. package/dist/harness/config.entity.d.ts +28 -28
  27. package/dist/harness/config.entity.js +4 -4
  28. package/dist/harness/plugin-stack.adapter.d.ts +24 -0
  29. package/dist/harness/plugin-stack.adapter.js +8 -0
  30. package/dist/harness/plugin-stack.adapter.test.d.ts +1 -0
  31. package/dist/harness/plugin.adapter.d.ts +15 -4
  32. package/dist/harness/plugin.adapter.js +18 -11
  33. package/dist/harness/plugin.adapter.test.d.ts +1 -0
  34. package/dist/harness/plugin.entity.d.ts +26 -10
  35. package/dist/harness/plugin.entity.js +1 -1
  36. package/dist/harness/session.entity.d.ts +20 -20
  37. package/dist/harness/session.entity.js +1 -1
  38. package/dist/harness/skill-files.d.ts +17 -0
  39. package/dist/harness/skill-files.js +25 -0
  40. package/dist/index.d.ts +47 -4
  41. package/dist/index.js +45 -4
  42. package/dist/loop/cacheKey.d.ts +3 -3
  43. package/dist/loop/cacheKey.js +4 -2
  44. package/dist/loop/mapping.d.ts +4 -5
  45. package/dist/loop/mapping.js +8 -16
  46. package/dist/loop/modelPolicy.d.ts +4 -4
  47. package/dist/loop/modelPolicy.js +2 -2
  48. package/dist/loop/promptProvenance.d.ts +2 -2
  49. package/dist/loop/promptProvenance.js +2 -2
  50. package/dist/loop/stepContext.d.ts +2 -2
  51. package/dist/loop/stepContext.js +2 -2
  52. package/dist/loop/streamFold.js +3 -3
  53. package/dist/loop/toolSchema.d.ts +17 -0
  54. package/dist/loop/toolSchema.js +25 -0
  55. package/dist/loop/toolSchema.test.d.ts +1 -0
  56. package/dist/mcp/bridge.d.ts +15 -15
  57. package/dist/mcp/bridge.js +14 -14
  58. package/dist/memory/memory-log.entity.d.ts +320 -0
  59. package/dist/memory/memory-log.entity.functions.d.ts +89 -0
  60. package/dist/memory/memory-log.entity.functions.js +180 -0
  61. package/dist/memory/memory-log.entity.js +75 -0
  62. package/dist/memory/memory-log.entity.test.d.ts +1 -0
  63. package/dist/memory/memory-session.d.ts +51 -0
  64. package/dist/memory/memory-session.js +97 -0
  65. package/dist/memory/memory-session.test.d.ts +1 -0
  66. package/dist/memory/memory.conformance.d.ts +32 -0
  67. package/dist/memory/memory.conformance.js +102 -0
  68. package/dist/ports/auth-store.port.d.ts +16 -18
  69. package/dist/ports/auth-store.port.js +13 -9
  70. package/dist/ports/capability.port.d.ts +131 -7
  71. package/dist/ports/capability.port.js +2 -2
  72. package/dist/ports/conversation-store.port.d.ts +54 -177
  73. package/dist/ports/conversation-store.port.js +9 -9
  74. package/dist/ports/file-system.port.d.ts +5 -7
  75. package/dist/ports/file-system.port.js +1 -1
  76. package/dist/ports/harness.port.d.ts +19 -13
  77. package/dist/ports/harness.port.js +10 -10
  78. package/dist/ports/mcp-client.port.d.ts +15 -47
  79. package/dist/ports/mcp-client.port.js +3 -3
  80. package/dist/ports/memory.port.d.ts +116 -0
  81. package/dist/ports/memory.port.js +7 -0
  82. package/dist/ports/model-catalog.port.d.ts +1 -1
  83. package/dist/ports/model-catalog.port.js +1 -1
  84. package/dist/ports/permission.port.d.ts +10 -0
  85. package/dist/ports/permission.port.js +4 -0
  86. package/dist/ports/run-context.port.d.ts +28 -0
  87. package/dist/ports/run-context.port.js +4 -0
  88. package/dist/ports/session-log.port.d.ts +25 -0
  89. package/dist/ports/session-log.port.js +4 -0
  90. package/dist/ports/sessions.port.d.ts +141 -0
  91. package/dist/ports/sessions.port.js +4 -0
  92. package/dist/ports/settings-store.port.d.ts +18 -56
  93. package/dist/ports/settings-store.port.js +3 -3
  94. package/dist/ports/shell.port.d.ts +9 -21
  95. package/dist/ports/shell.port.js +1 -1
  96. package/dist/ports/step-loop.port.d.ts +85 -0
  97. package/dist/ports/step-loop.port.js +4 -0
  98. package/dist/ports/tasks.port.d.ts +59 -0
  99. package/dist/ports/tasks.port.js +10 -0
  100. package/dist/ports/tool-registry.port.d.ts +53 -0
  101. package/dist/ports/tool-registry.port.js +4 -0
  102. package/dist/ports/turn-admission.port.d.ts +23 -0
  103. package/dist/ports/turn-admission.port.js +4 -0
  104. package/dist/ports/turn-events.port.d.ts +55 -0
  105. package/dist/ports/turn-events.port.js +7 -0
  106. package/dist/ports/turn-scope.port.d.ts +101 -0
  107. package/dist/ports/turn-scope.port.js +10 -0
  108. package/dist/ports/turn.port.d.ts +113 -0
  109. package/dist/ports/turn.port.js +0 -0
  110. package/dist/ports/utility-llm.port.d.ts +12 -31
  111. package/dist/ports/utility-llm.port.js +1 -1
  112. package/dist/session/chassis.js +7 -7
  113. package/dist/session/session-event.entity.d.ts +55 -0
  114. package/dist/session/session-event.entity.functions.d.ts +31 -0
  115. package/dist/session/session-event.entity.functions.js +42 -0
  116. package/dist/session/session-event.entity.js +37 -0
  117. package/dist/session/session-event.entity.test.d.ts +1 -0
  118. package/dist/session/session-log.conformance.d.ts +13 -0
  119. package/dist/session/session-log.conformance.js +167 -0
  120. package/dist/session/session-log.entity.d.ts +164 -0
  121. package/dist/session/session-log.entity.js +82 -0
  122. package/dist/session/session-log.memory.adapter.d.ts +7 -0
  123. package/dist/session/session-log.memory.adapter.js +82 -0
  124. package/dist/session/session-log.memory.adapter.test.d.ts +1 -0
  125. package/dist/session/sessions.entity.d.ts +201 -0
  126. package/dist/session/sessions.entity.js +98 -0
  127. package/dist/session/tasks.entity.d.ts +39 -0
  128. package/dist/session/tasks.entity.js +26 -0
  129. package/dist/session/turn-admission.open.adapter.d.ts +4 -0
  130. package/dist/session/turn-admission.open.adapter.js +3 -0
  131. package/dist/spec/SpecDoc.d.ts +30 -117
  132. package/dist/spec/SpecDoc.js +10 -10
  133. package/dist/spec/codec.js +1 -1
  134. package/dist/turn/model-request.entity.d.ts +99 -0
  135. package/dist/turn/model-request.entity.functions.d.ts +108 -0
  136. package/dist/turn/model-request.entity.functions.js +167 -0
  137. package/dist/turn/model-request.entity.js +41 -0
  138. package/dist/turn/model-request.entity.test.d.ts +1 -0
  139. package/dist/turn/prompt-sections.d.ts +10 -0
  140. package/dist/turn/prompt-sections.js +3 -0
  141. package/dist/turn/step-loop.conformance.d.ts +16 -0
  142. package/dist/turn/step-loop.conformance.js +164 -0
  143. package/dist/turn/testing.d.ts +30 -0
  144. package/dist/turn/turn-bus.d.ts +23 -0
  145. package/dist/turn/turn-bus.js +54 -0
  146. package/dist/turn/turn-bus.test.d.ts +1 -0
  147. package/dist/turn/turn-event.entity.d.ts +263 -0
  148. package/dist/turn/turn-event.entity.functions.d.ts +35 -0
  149. package/dist/turn/turn-event.entity.functions.js +21 -0
  150. package/dist/turn/turn-event.entity.js +101 -0
  151. package/dist/turn/turn-lifecycle.d.ts +32 -0
  152. package/dist/turn/turn-lifecycle.js +48 -0
  153. package/dist/turn/turn-run.d.ts +26 -0
  154. package/dist/turn/turn-run.js +105 -0
  155. package/dist/turn/turn.adapter.d.ts +21 -0
  156. package/dist/turn/turn.adapter.js +126 -0
  157. package/dist/turn/turn.adapter.test.d.ts +1 -0
  158. package/dist/turn/turn.conformance.d.ts +20 -0
  159. package/dist/turn/turn.conformance.js +74 -0
  160. package/dist/turn/user-message.entity.d.ts +14 -0
  161. package/dist/turn/user-message.entity.js +10 -0
  162. package/dist/util/json.d.ts +2 -2
  163. package/dist/util/json.js +10 -10
  164. package/package.json +3 -3
@@ -0,0 +1,105 @@
1
+ import { Context, Effect, Option } from "effect";
2
+ import { LanguageModel } from "effect/ai";
3
+ import { toolParametersSchema } from "../loop/toolSchema.js";
4
+ import { HarnessError } from "../harness/plugin.entity.js";
5
+ import { canonicalJson, estimateTokens, fingerprintOf } from "../memory/memory-log.entity.functions.js";
6
+ import { StepLoop } from "../ports/step-loop.port.js";
7
+ import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
8
+ import { TurnMemory, TurnPrompt, TurnToolbox } from "../ports/turn-scope.port.js";
9
+ import { CurrentModelCallPolicy } from "../loop/modelPolicy.js";
10
+ import {
11
+ checkModelRequest,
12
+ modelRequestDescriptorOf,
13
+ modelRequestHeaderOf,
14
+ modelRequestTools,
15
+ reconstructModelRequest,
16
+ resolveModelRequest
17
+ } from "./model-request.entity.functions.js";
18
+ const defaultLimits = { maxSteps: 50, toolConcurrency: 1, streaming: !0, requireCompletion: !1 }, defaultBudgetTokens = 64000, incomplete = { complete: !1, awaiting: [], facts: {} }, noDirective = { context: Option.none(), toolChoice: Option.none() };
19
+ export const schemaTokens = (tools, active) => active.reduce((sum, name) => {
20
+ const tool = tools.toolkit.tools[name];
21
+ return tool === void 0 ? sum : sum + estimateTokens(`${tool.description ?? ""}${canonicalJson(toolParametersSchema(tool))}`);
22
+ }, 0), cacheKeyOf = (prefix, conversation) => Option.map(Option.filter(Option.some(prefix), (value) => value.length > 0), (value) => `${value}:${conversation}`), stepRequestOf = (policy, options = {}) => Effect.gen(function* () {
23
+ const runServices = yield* Effect.context(), inRun = (effect) => effect.pipe(Effect.provide(runServices)), memory = yield* TurnMemory, prompt = yield* TurnPrompt, events = yield* TurnEvents, tasks = yield* TurnTasks, tools = yield* (yield* TurnToolbox).tools, session = memory.session, runId = (yield* session.renderRecipe("none")).currentRun, defaultModel = Context.getOption(runServices, LanguageModel.LanguageModel), number = yield* memory.number, limits = { ...defaultLimits, ...options.limits, ...policy.limits }, budget = policy.budgetTokens ?? options.budgetTokens ?? defaultBudgetTokens, stepContext = policy.stepContext ?? "tail";
24
+ yield* Option.match(Option.fromNullishOr(policy.initial), {
25
+ onNone: () => Effect.void,
26
+ onSome: (batch) => batch.skills.length === 0 ? Effect.void : tools.activate(batch.skills, "host").pipe(Effect.asVoid)
27
+ });
28
+ yield* inRun(prompt.turnSections);
29
+ yield* session.maintain({ phase: "turn-start", lastUsage: Option.none(), budgetTokens: budget, views: tools.views });
30
+ const plan = (info) => Effect.gen(function* () {
31
+ const choice = policy.model === void 0 ? Option.none() : yield* inRun(policy.model(info)), system = yield* inRun(prompt.system(Option.flatMap(choice, (value) => value.variant))), directive = policy.step === void 0 ? noDirective : yield* inRun(policy.step(info));
32
+ yield* Option.match(directive.context, {
33
+ onNone: () => Effect.void,
34
+ onSome: (text) => session.record([{ _tag: "StepContext", step: info.stepIndex, text }], info.stepIndex).pipe(Effect.asVoid)
35
+ });
36
+ const reserved = estimateTokens(system) + schemaTokens(tools, info.activeTools);
37
+ yield* session.maintain({ phase: "step", lastUsage: info.lastUsage, budgetTokens: Math.max(1, budget - reserved), views: tools.views });
38
+ const built = yield* session.build({ stepContext: stepContext === "tail" ? "tail" : "none" }), render = yield* session.renderRecipe(stepContext === "tail" ? "tail" : "none");
39
+ yield* events.publish({
40
+ _tag: "context.built",
41
+ step: info.stepIndex,
42
+ turn: number,
43
+ strategy: session.strategy.id,
44
+ strategyVersion: session.strategy.version,
45
+ fingerprint: built.fingerprint,
46
+ systemFingerprint: fingerprintOf(system),
47
+ estimatedTokens: built.estimatedTokens,
48
+ reservedTokens: reserved,
49
+ compactions: built.compactions.length,
50
+ activeTools: info.activeTools
51
+ });
52
+ const stepText = stepContext === "system" ? Option.getOrElse(directive.context, () => "") : "", finalSystem = [system, stepText].filter((part) => part.length > 0).join(`
53
+
54
+ `), model = yield* Option.match(Option.orElse(Option.map(choice, (value) => value.model), () => defaultModel), {
55
+ onNone: () => Effect.succeed(Option.none()),
56
+ onSome: (chosen) => resolveModelRequest(chosen).pipe(Effect.map(Option.some))
57
+ });
58
+ yield* memory.prepareRequest({
59
+ version: 1,
60
+ runId,
61
+ step: info.stepIndex,
62
+ strategyVersion: session.strategy.version,
63
+ system: finalSystem,
64
+ render,
65
+ through: built.through,
66
+ contextFingerprint: built.fingerprint,
67
+ tools: modelRequestTools(info.activeTools.flatMap((name) => tools.toolkit.tools[name] === void 0 ? [] : [tools.toolkit.tools[name]])),
68
+ toolChoice: Option.getOrElse(directive.toolChoice, () => "auto"),
69
+ model: yield* Option.match(model, { onNone: () => Effect.succeed(Option.none()), onSome: modelRequestDescriptorOf }),
70
+ cacheKey: options.cacheKey ?? Option.none(),
71
+ callPolicy: yield* Effect.service(CurrentModelCallPolicy)
72
+ });
73
+ return {
74
+ model,
75
+ system: finalSystem,
76
+ messages: built.messages,
77
+ toolChoice: directive.toolChoice
78
+ };
79
+ });
80
+ return {
81
+ tools,
82
+ handlers: Context.merge(runServices, tools.handlers),
83
+ limits,
84
+ initial: Option.fromNullishOr(policy.initial),
85
+ plan,
86
+ dispatch: (step, actual) => Effect.gen(function* () {
87
+ const stored = yield* memory.requestSnapshot, header = yield* modelRequestHeaderOf(stored.requests, runId, step), expected = yield* reconstructModelRequest(header, stored.entries);
88
+ yield* checkModelRequest(expected, {
89
+ prompt: actual.prompt,
90
+ tools: modelRequestTools(actual.tools),
91
+ toolChoice: actual.toolChoice,
92
+ model: yield* modelRequestDescriptorOf(actual.model),
93
+ cacheKey: options.cacheKey ?? Option.none(),
94
+ callPolicy: yield* Effect.service(CurrentModelCallPolicy)
95
+ });
96
+ }),
97
+ record: (step, tail) => session.recordTail(tail, tools.views, step),
98
+ completion: (info) => policy.completion === void 0 ? Effect.succeed(incomplete) : inRun(policy.completion(info)),
99
+ steering: options.steering ?? Effect.succeed(Option.none()),
100
+ correctives: Option.fromNullishOr(policy.correctives),
101
+ events,
102
+ tasks,
103
+ cacheKey: options.cacheKey ?? Option.none()
104
+ };
105
+ }), runTurnLoop = (policy, options = {}) => stepRequestOf(policy, options).pipe(Effect.flatMap((request) => StepLoop.pipe(Effect.flatMap((loop) => loop.run(request)))));
@@ -0,0 +1,21 @@
1
+ import { Layer } from "effect";
2
+ import { HarnessError } from "../harness/plugin.entity.js";
3
+ import { ConversationMemory } from "../ports/memory.port.js";
4
+ import { RunContext } from "../ports/run-context.port.js";
5
+ import { ToolRegistry } from "../ports/tool-registry.port.js";
6
+ import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
7
+ import { TurnMemory, TurnPrompt, TurnToolbox } from "../ports/turn-scope.port.js";
8
+ import type { TurnLiveInput } from "../ports/turn-scope.port.js";
9
+ /**
10
+ * One admitted turn's services, over the turn's writer, built in this order:
11
+ * the event bus and the tasks, with the writer as the bus's FIRST
12
+ * subscriber (every stored event is queued before any reaction runs); then
13
+ * the memory session, opened over the session's earlier memory events where
14
+ * this layer is built (a digester or summarizer there is used), and
15
+ * RunContext. It also holds the tools slot (`TurnToolbox`) and the prompt
16
+ * state (`TurnPrompt`). The message was stored when the turn began; memory
17
+ * takes it at `TurnMemory.persistMessage`, and records nothing before it
18
+ * (`TurnMemory.context` waits for it). Tasks run in the layer's scope:
19
+ * they are interrupted when it closes.
20
+ */
21
+ export declare const TurnLive: (input: TurnLiveInput) => Layer.Layer<RunContext | TurnEvents | TurnTasks | TurnMemory | TurnToolbox | TurnPrompt, HarnessError, ConversationMemory | ToolRegistry>;
@@ -0,0 +1,126 @@
1
+ import { Context, Effect, Layer, Option, Ref, Schema, Scope, Semaphore } from "effect";
2
+ import { HarnessError } from "../harness/plugin.entity.js";
3
+ import { fingerprintOf } from "../memory/memory-log.entity.functions.js";
4
+ import { readerOf } from "../memory/memory-session.js";
5
+ import { Capabilities } from "../ports/capability.port.js";
6
+ import { ConversationMemory } from "../ports/memory.port.js";
7
+ import { RunContext } from "../ports/run-context.port.js";
8
+ import { draftOfTurnEvent, draftsOfEntries, entriesOfEvents } from "../session/session-event.entity.functions.js";
9
+ import { MEMORY_KINDS, RESERVED_KINDS } from "../session/session-event.entity.js";
10
+ import { ToolRegistry } from "../ports/tool-registry.port.js";
11
+ import { TurnEvents, TurnTasks } from "../ports/turn-events.port.js";
12
+ import { TurnMemory, TurnPrompt, TurnToolbox } from "../ports/turn-scope.port.js";
13
+ import { renderSections } from "./prompt-sections.js";
14
+ import { makeTurnEvents, makeTurnTasks } from "./turn-bus.js";
15
+ import { ModelRequestHeader } from "./model-request.entity.js";
16
+ const failure = (code, message) => new HarnessError({ code, message }), inCaller = (effect) => Effect.flatMap(Effect.context(), (context) => effect.pipe(Effect.provide(context))), storedDraft = (turn) => (event) => event._tag === "host" && RESERVED_KINDS.includes(event.name) ? Option.some(failure("events.reserved", `${event.name} is the framework's kind`)) : Option.map(draftOfTurnEvent(turn, event), (draft) => ({ kind: draft.kind, data: draft.data })), contextBody = (entry) => ({ _tag: "TurnContext", sectionId: entry.id, version: entry.version, text: entry.text });
17
+ export const TurnLive = (input) => Layer.effectContext(Effect.gen(function* () {
18
+ const scope = yield* Effect.scope, memory = yield* ConversationMemory, registry = yield* ToolRegistry, sections = Option.getOrElse(yield* Effect.serviceOption(Capabilities), () => []).flatMap((capability) => capability.promptSections), writer = input.turn, admitted = writer.admitted, events = yield* makeTurnEvents({ maxDepth: input.maxEventDepth ?? 8 }), tasks = yield* makeTurnTasks(scope);
19
+ yield* events.subscribe(storedDraft(admitted.turn), (draft) => draft instanceof HarnessError ? Effect.fail(draft) : writer.append([draft]));
20
+ const message = yield* Ref.make({ _tag: "Waiting", early: [] }), history = yield* Ref.make(Option.none()), decoded = (stored) => entriesOfEvents(stored).pipe(Effect.mapError((error) => failure("memory.log", `undecodable memory events: ${error.message}`))), readHistory = writer.history(MEMORY_KINDS).pipe(Effect.flatMap(decoded)), log = {
21
+ read: readHistory.pipe(Effect.tap((entries) => Ref.set(history, Option.some(entries)))),
22
+ append: (entries) => Effect.gen(function* () {
23
+ if ((yield* Ref.get(message))._tag === "Waiting" && entries.some((entry) => entry.body._tag !== "TurnStarted"))
24
+ return yield* Effect.fail(failure("turn.unstarted", "memory records nothing before the turn's message is taken (persistMessage); TurnMemory.context waits for it"));
25
+ const started = entries.filter((entry) => entry.body._tag === "TurnStarted");
26
+ if (started.some((entry) => entry.turn !== admitted.turn || entry.id !== writer.started.data.entry))
27
+ 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 ${String(writer.started.data.entry)} of turn ${admitted.turn}`));
28
+ const drafts = yield* draftsOfEntries(entries).pipe(Effect.mapError((error) => failure("memory.log", error.message)));
29
+ yield* writer.append(drafts.map((draft) => ({ kind: draft.kind, data: draft.data })));
30
+ })
31
+ }, session = yield* memory.open({ conversation: admitted.session.id, runId: admitted.runId, log }), reader = readerOf(session), slot = yield* Ref.make(Option.none()), openTools = Ref.get(slot).pipe(Effect.flatMap(Option.match({
32
+ onNone: () => Effect.fail(failure("tools.unavailable", "Tools are not open yet")),
33
+ onSome: (tools) => Effect.succeed(tools)
34
+ }))), run = RunContext.of({
35
+ conversation: admitted.session.id,
36
+ session: admitted.session,
37
+ runId: admitted.runId,
38
+ userMessage: admitted.userMessage,
39
+ memory: reader,
40
+ events,
41
+ tasks,
42
+ activate: (skills) => openTools.pipe(Effect.flatMap((tools) => tools.activate(skills, "host"))),
43
+ flush: writer.flush,
44
+ write: writer.write
45
+ }), requestKinds = [...MEMORY_KINDS, "request.prepared"], snapshots = yield* Semaphore.make(1), snapshot = yield* Ref.make(Option.none()), requestSnapshot = snapshots.withPermits(1)(Effect.gen(function* () {
46
+ const known = yield* Option.match(yield* Ref.get(snapshot), {
47
+ onNone: () => Ref.get(history).pipe(Effect.flatMap(Option.match({ onNone: () => readHistory, onSome: Effect.succeed })), Effect.map((entries) => ({ entries, requests: [], cursor: writer.started.seq - 1 }))),
48
+ onSome: Effect.succeed
49
+ }), stored = yield* writer.snapshot(requestKinds, known.cursor), next = {
50
+ entries: [...known.entries, ...yield* decoded(stored.filter((event) => MEMORY_KINDS.includes(event.kind)))],
51
+ requests: [...known.requests, ...stored.filter((event) => event.kind === "request.prepared" && event.data.runId === admitted.runId)],
52
+ cursor: stored.reduce((last, event) => Math.max(last, event.seq), known.cursor)
53
+ };
54
+ yield* Ref.set(snapshot, Option.some(next));
55
+ return { entries: next.entries, requests: next.requests };
56
+ })), claimed = yield* Ref.make(!1), replied = yield* Ref.make(!1), taken = Ref.get(message).pipe(Effect.map((state) => state._tag === "Taken" ? Option.some(state.number) : Option.none())), turnMemory = TurnMemory.of({
57
+ ...reader,
58
+ strategy: session.strategy,
59
+ session,
60
+ prepareRequest: (header) => Schema.encodeEffect(ModelRequestHeader)(header).pipe(Effect.mapError((error) => failure("request.encode", error.message)), Effect.flatMap((data) => writer.append([{ kind: "request.prepared", data }])), Effect.andThen(writer.flush)),
61
+ requestSnapshot,
62
+ number: taken.pipe(Effect.flatMap(Option.match({
63
+ onNone: () => Effect.fail(failure("turn.unstarted", "The user's message is not persisted yet")),
64
+ onSome: (number) => Effect.succeed(number)
65
+ }))),
66
+ persistMessage: Effect.gen(function* () {
67
+ if (yield* Ref.getAndSet(claimed, !0))
68
+ return yield* Effect.fail(failure("turn.persisted", "The user's message is already persisted"));
69
+ const number = admitted.turn;
70
+ yield* session.record([{ _tag: "TurnStarted", userMessage: admitted.userMessage }], 0);
71
+ const early = yield* Ref.modify(message, (state) => [state._tag === "Waiting" ? state.early : [], { _tag: "Taken", number }]);
72
+ yield* early.length === 0 ? Effect.void : session.record(early.map(contextBody), 0);
73
+ yield* events.publish({ _tag: "turn.started", runId: admitted.runId, turn: number, userMessage: admitted.userMessage });
74
+ return number;
75
+ }),
76
+ context: (entry) => Ref.modify(message, (state) => state._tag === "Waiting" ? [!1, { _tag: "Waiting", early: [...state.early, entry] }] : [!0, state]).pipe(Effect.flatMap((now) => now ? session.record([contextBody(entry)], 0).pipe(Effect.asVoid) : Effect.void)),
77
+ persistReply: (outcome) => Effect.gen(function* () {
78
+ const number = yield* taken;
79
+ if (Option.isNone(number) || (yield* Ref.getAndSet(replied, !0)))
80
+ return;
81
+ yield* session.record([{ _tag: "TurnEnded", outcome: outcome.outcome, reply: outcome.reply }], 0);
82
+ yield* events.publish({ _tag: "turn.ended", runId: admitted.runId, turn: number.value, outcome: outcome.outcome, reply: outcome.reply });
83
+ })
84
+ }), opened = yield* Ref.make(!1), toolbox = TurnToolbox.of({
85
+ open: Effect.gen(function* () {
86
+ if (yield* Ref.getAndSet(opened, !0))
87
+ return yield* Effect.fail(failure("tools.opened", "The turn's tools are already open"));
88
+ const tools = yield* registry.open(session).pipe(Effect.provideService(RunContext, run), Scope.provide(scope));
89
+ yield* Ref.set(slot, Option.some(tools));
90
+ return tools;
91
+ }),
92
+ tools: openTools
93
+ }), promptContext = (tools, variant) => tools.active.pipe(Effect.map((active) => ({ variant, active, skills: tools.skills }))), rendered = (tools, variant, tiers) => inCaller(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), turnSections = yield* Ref.make(!1), prompt = TurnPrompt.of({
94
+ system: (variant) => Effect.gen(function* () {
95
+ const key = Option.getOrElse(variant, () => ""), known = (yield* Ref.get(systems)).get(key);
96
+ if (known !== void 0)
97
+ return known;
98
+ 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(`
99
+
100
+ `), fingerprint = fingerprintOf(text);
101
+ if (!Option.contains(yield* Ref.get(lastSystem), fingerprint)) {
102
+ yield* session.record([{
103
+ _tag: "SystemPrepared",
104
+ fingerprint,
105
+ text,
106
+ sections: parts.map((part) => ({ id: part.section.id, version: part.section.version, fingerprint: fingerprintOf(part.text) }))
107
+ }], 0);
108
+ yield* Ref.set(lastSystem, Option.some(fingerprint));
109
+ }
110
+ yield* Ref.update(systems, (all) => new Map([...all, [key, text]]));
111
+ return text;
112
+ }),
113
+ turnSections: Effect.gen(function* () {
114
+ if (yield* Ref.getAndSet(turnSections, !0))
115
+ return;
116
+ const parts = yield* rendered(yield* openTools, Option.none(), (section) => section.tier === "turn");
117
+ yield* parts.length === 0 ? Effect.void : session.record(parts.map(({ section, text }) => ({
118
+ _tag: "TurnContext",
119
+ sectionId: section.id,
120
+ version: section.version,
121
+ text
122
+ })), 0);
123
+ })
124
+ });
125
+ return Context.make(RunContext, run).pipe(Context.add(TurnEvents, events), Context.add(TurnTasks, tasks), Context.add(TurnMemory, turnMemory), Context.add(TurnToolbox, toolbox), Context.add(TurnPrompt, prompt));
126
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { Context, Effect } from "effect";
2
+ import type { Scope } from "effect";
3
+ import type { ConformanceCheck } from "../conformance.entity.js";
4
+ import { HarnessError } from "../harness/plugin.entity.js";
5
+ import type { TurnRunner } from "../ports/turn.port.js";
6
+ /**
7
+ * The turn contract, as checks any composition of the turn must pass (an
8
+ * `Agent`, or a host's own composition of `TurnLive` and the lifecycle):
9
+ * the session's writer is the bus's first subscriber; the user's message
10
+ * is in memory before the matcher reads the history; the reply is recorded
11
+ * and the turn ended exactly once on success, failure and interrupt; tasks
12
+ * are joined before the reply; and the host layer sees only earlier turns.
13
+ *
14
+ * `runner` is acquired afresh for each check. `services` are merged into
15
+ * every turn's services (the model, the `Sessions` the turns begin with, and
16
+ * whatever else the runner requires); each check makes its own session, and
17
+ * the kit adds its own IntentMatcher, which the runner's tools must consult
18
+ * (plugin-tool-discovery does).
19
+ */
20
+ export declare const turnConformance: (runner: Effect.Effect<TurnRunner, HarnessError, Scope.Scope>, services?: Context.Context<never>) => ReadonlyArray<ConformanceCheck>;
@@ -0,0 +1,74 @@
1
+ import { Context, Deferred, Effect, Fiber, Layer, Option, Ref } from "effect";
2
+ import { ConformanceFailure } from "../conformance.entity.js";
3
+ import { HarnessError } from "../harness/plugin.entity.js";
4
+ import { IntentMatcher } from "../ports/capability.port.js";
5
+ import { RunContext } from "../ports/run-context.port.js";
6
+ import { Sessions } from "../ports/sessions.port.js";
7
+ import { UserMessage } from "./user-message.entity.js";
8
+ const fail = (check) => (message) => Effect.fail(new ConformanceFailure({ check, message })), expect = (check, holds, message) => holds ? Effect.void : fail(check)(message), stored = (session) => session.sessions.read(session.address).pipe(Effect.mapError((error) => new HarnessError({ code: "conformance.session", message: error._tag }))), ofRun = (all, runId) => {
9
+ const turn = Option.flatMap(Option.fromNullishOr(all.find((event) => event.kind === "turn.started" && event.data.runId === runId)), (event) => event.turn);
10
+ return Option.match(turn, { onNone: () => [], onSome: (number) => all.filter((event) => Option.contains(event.turn, number)) });
11
+ }, names = (session, runId) => stored(session).pipe(Effect.map((all) => ofRun(all, runId).map((event) => event.kind)));
12
+ export const turnConformance = (runner, services = Context.empty()) => {
13
+ const check = (name, id, body) => ({
14
+ name,
15
+ run: Effect.scoped(Effect.gen(function* () {
16
+ const history = yield* Ref.make([]), matcher = Context.make(IntentMatcher, IntentMatcher.of({
17
+ id: "conformance",
18
+ version: "1",
19
+ match: (request) => Ref.update(history, (all) => [...all, request.history]).pipe(Effect.as({ skills: [], probabilities: Option.none(), abstained: !0 }))
20
+ })), sessions = yield* Option.match(Context.getOption(services, Sessions), {
21
+ onNone: () => fail(id)("the turn services hold no Sessions to begin turns with"),
22
+ onSome: (found) => Effect.succeed(found)
23
+ }), view = yield* sessions.create({ owner: "conformance" }).pipe(Effect.mapError((error) => new HarnessError({ code: "conformance.session", message: error._tag }))), acquired = yield* runner;
24
+ yield* body({
25
+ turn: (input, use) => acquired.turn({ ...input, services: Context.merge(Context.merge(services, matcher), input.services) }, use)
26
+ }, { sessions, address: { id: view.header.id, owner: "conformance" } }, history);
27
+ })).pipe(Effect.catchTag("HarnessError", (error) => fail(id)(`${error.code}: ${error.message}`)))
28
+ }), input = (session, runId) => ({
29
+ turn: { session: session.address, userMessage: new UserMessage({ text: `the message of ${runId}` }), runId },
30
+ services: Context.empty()
31
+ }), reply = (text) => Effect.succeed({ outcome: "completed", reply: Option.some(text) });
32
+ return [
33
+ check("the session's writer is the bus's first subscriber", "journal-first", (turns, session) => Effect.gen(function* () {
34
+ const outcome = yield* turns.turn(input(session, "run-1"), (turn) => Effect.gen(function* () {
35
+ const journaled = yield* Ref.make(!1);
36
+ yield* turn.events.subscribe((event) => event._tag === "host" && event.name === "conformance.probe" ? Option.some(event) : Option.none(), () => turn.flush.pipe(Effect.andThen(names(session, "run-1")), Effect.flatMap((kinds) => Ref.set(journaled, kinds.includes("conformance.probe")))));
37
+ yield* turn.events.publish({ _tag: "host", name: "conformance.probe", data: {} });
38
+ return yield* reply((yield* Ref.get(journaled)) ? "journaled first" : "not journaled");
39
+ }));
40
+ yield* expect("journal-first", Option.contains(outcome.reply, "journaled first"), "a subscriber ran before the session stored the event it received");
41
+ })),
42
+ check("the user's message is recorded before the matcher reads the history", "message-first", (turns, session, history) => Effect.gen(function* () {
43
+ yield* turns.turn(input(session, "run-1"), (turn) => turn.tools.match(turn.userMessage).pipe(Effect.andThen(reply("matched"))));
44
+ const seen = yield* Ref.get(history), last = Option.fromNullishOr(seen.at(-1)?.at(-1));
45
+ yield* expect("message-first", seen.length === 1, `the matcher was consulted ${seen.length} times, not once`);
46
+ yield* expect("message-first", Option.exists(last, (message) => message.role === "user" && JSON.stringify(message.content).includes("the message of run-1")), "the matcher's history does not end with the turn's own message");
47
+ })),
48
+ check("the reply is recorded and the turn ended exactly once, on success, failure and interrupt", "ended-once", (turns, session) => Effect.gen(function* () {
49
+ yield* turns.turn(input(session, "run-1"), () => reply("done"));
50
+ yield* Effect.result(turns.turn(input(session, "run-2"), () => Effect.fail(new HarnessError({ code: "conformance.host", message: "the host failed" }))));
51
+ const started = yield* Deferred.make(), fiber = yield* Effect.forkChild(turns.turn(input(session, "run-3"), () => Deferred.succeed(started, void 0).pipe(Effect.andThen(Effect.never))));
52
+ yield* Deferred.await(started);
53
+ yield* Fiber.interrupt(fiber);
54
+ const all = yield* stored(session), per = (runId, kind, field) => ofRun(all, runId).filter((event) => event.kind === kind).map(field).join("+"), replies = ["run-1", "run-2", "run-3"].map((runId) => `${runId}:${per(runId, "turn.reply", (event) => event.data.body?.outcome)}`), endings = ["run-1", "run-2", "run-3"].map((runId) => `${runId}:${per(runId, "turn.ended", (event) => event.data.reason)}`);
55
+ yield* expect("ended-once", replies.join() === "run-1:completed,run-2:failed,run-3:failed", `the replies were ${replies.join()}`);
56
+ yield* expect("ended-once", endings.join() === "run-1:completed,run-2:failed,run-3:interrupted", `the turns ended ${endings.join()}`);
57
+ })),
58
+ check("tasks are joined before the reply", "tasks-joined", (turns, session) => Effect.gen(function* () {
59
+ yield* turns.turn(input(session, "run-1"), (turn) => turn.tasks.fork("late", Effect.sleep("10 millis").pipe(Effect.andThen(turn.events.publish({ _tag: "host", name: "conformance.task", data: {} })))).pipe(Effect.andThen(reply("forked"))));
60
+ const kinds = yield* names(session, "run-1");
61
+ yield* expect("tasks-joined", kinds.includes("conformance.task") && kinds.indexOf("conformance.task") < kinds.indexOf("turn.reply"), `the task's event came ${kinds.includes("conformance.task") ? "after the reply" : "never"}`);
62
+ })),
63
+ check("the host layer is built before the message: it sees only earlier turns", "layer-before-message", (turns, session) => Effect.gen(function* () {
64
+ const seen = yield* Ref.make([]), layer = Layer.effectDiscard(Effect.gen(function* () {
65
+ const run = yield* RunContext, own = (yield* run.memory.entries).filter((entry) => entry.runId === run.runId).length, turn = yield* run.memory.turn;
66
+ yield* Ref.update(seen, (all) => [...all, `${run.runId}:turn ${turn}, own entries ${own}`]);
67
+ }));
68
+ yield* turns.turn({ ...input(session, "run-1"), layer }, () => reply("first"));
69
+ yield* turns.turn({ ...input(session, "run-2"), layer }, () => reply("second"));
70
+ const built = (yield* Ref.get(seen)).join("; ");
71
+ yield* expect("layer-before-message", built === "run-1:turn 0, own entries 0; run-2:turn 1, own entries 0", `the host layer saw ${built}`);
72
+ }))
73
+ ];
74
+ };
@@ -0,0 +1,14 @@
1
+ import { Schema } from "effect";
2
+ declare const UserMessage_base: Schema.Class<UserMessage, Schema.Struct<{
3
+ readonly text: Schema.NonEmptyString;
4
+ }>, {}>;
5
+ /**
6
+ * What the user said to start a turn. A prompt is text sent to a model; the
7
+ * user's message is a value of its own, and every field or parameter holding
8
+ * it is named `userMessage`. Attachments may join `text` later.
9
+ */
10
+ export declare class UserMessage extends UserMessage_base {
11
+ }
12
+ /** A `UserMessage` stored as its plain text, for records that keep a string on the wire. */
13
+ export declare const UserMessageFromString: Schema.decodeTo<typeof UserMessage, Schema.String, never, never>;
14
+ export {};
@@ -0,0 +1,10 @@
1
+ import { Schema, SchemaTransformation } from "effect";
2
+
3
+ export class UserMessage extends Schema.Class("UserMessage")({
4
+ text: Schema.NonEmptyString
5
+ }) {
6
+ }
7
+ export const UserMessageFromString = Schema.String.pipe(Schema.decodeTo(UserMessage, SchemaTransformation.transform({
8
+ decode: (text) => ({ text }),
9
+ encode: (userMessage) => userMessage.text
10
+ })));
@@ -1,4 +1,4 @@
1
- import { Effect, Either, Option } from "effect";
1
+ import { Effect, Result, Option } from "effect";
2
2
  /**
3
3
  * Parse JSON with CORRUPT ≠ ABSENT semantics: malformed text logs a warning
4
4
  * naming the source and yields `None`. Config readers fall back to their
@@ -16,4 +16,4 @@ export declare const parseJsonOption: (text: string) => Option.Option<unknown>;
16
16
  * `decode`; a corrupt or undecodable LINE is skipped (append-only files
17
17
  * corrupt at a line boundary — one bad row must not brick the history).
18
18
  * The shared machinery of the smith memory ledger and the social ledger. */
19
- export declare const decodeJsonLines: <A, E>(text: string, decode: (value: unknown) => Either.Either<A, E>) => ReadonlyArray<A>;
19
+ export declare const decodeJsonLines: <A, E>(text: string, decode: (value: unknown) => Result.Result<A, E>) => ReadonlyArray<A>;
package/dist/util/json.js CHANGED
@@ -1,15 +1,15 @@
1
- import { Effect, Either, Option } from "effect";
2
- export const parseJsonWarn = (text, where) => Either.match(Either.try(() => JSON.parse(text)), {
3
- onLeft: (error) => Effect.logWarning(`${where}: unreadable JSON \u2014 treating as empty: ${String(error)}`).pipe(Effect.as(Option.none())),
4
- onRight: (value) => Effect.succeed(Option.some(value))
1
+ import { Effect, Result, Option } from "effect";
2
+ export const parseJsonWarn = (text, where) => Result.match(Result.try(() => JSON.parse(text)), {
3
+ onFailure: (error) => Effect.logWarning(`${where}: unreadable JSON \u2014 treating as empty: ${String(error)}`).pipe(Effect.as(Option.none())),
4
+ onSuccess: (value) => Effect.succeed(Option.some(value))
5
5
  }), asJsonRecord = (value) => Option.match(value, {
6
6
  onNone: () => ({}),
7
7
  onSome: (parsed) => typeof parsed === "object" && parsed !== null ? parsed : {}
8
- }), parseJsonOption = (text) => Either.getRight(Either.try(() => JSON.parse(text))), decodeJsonLines = (text, decode) => text.split(`
9
- `).filter((line) => line.trim().length > 0).flatMap((line) => Either.try(() => JSON.parse(line)).pipe(Either.match({
10
- onLeft: () => [],
11
- onRight: (parsed) => Either.match(decode(parsed), {
12
- onLeft: () => [],
13
- onRight: (decoded) => [decoded]
8
+ }), parseJsonOption = (text) => Result.getSuccess(Result.try(() => JSON.parse(text))), decodeJsonLines = (text, decode) => text.split(`
9
+ `).filter((line) => line.trim().length > 0).flatMap((line) => Result.try(() => JSON.parse(line)).pipe(Result.match({
10
+ onFailure: () => [],
11
+ onSuccess: (parsed) => Result.match(decode(parsed), {
12
+ onFailure: () => [],
13
+ onSuccess: (decoded) => [decoded]
14
14
  })
15
15
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xandreed/core",
3
- "version": "0.4.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The agent kernel of the new line: entities, ports, the provider-agnostic agent loop, and the session chassis — pure Effect, no IO, no provider SDKs.",
@@ -12,8 +12,7 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@effect/ai": "0.35.0",
16
- "effect": "3.21.4"
15
+ "effect": "4.0.0-rc.118"
17
16
  },
18
17
  "types": "./dist/index.d.ts",
19
18
  "files": [
@@ -24,6 +23,7 @@
24
23
  "LICENSE"
25
24
  ],
26
25
  "engines": {
26
+ "node": ">=24",
27
27
  "bun": ">=1.3.0"
28
28
  },
29
29
  "license": "MIT"