@kuralle-syrinx/kuralle 3.0.0 → 4.0.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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@kuralle-syrinx/kuralle",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.ts",
8
8
  "types": "./src/index.ts",
9
9
  "dependencies": {
10
- "@kuralle-syrinx/core": "3.0.0"
10
+ "@kuralle-syrinx/core": "4.0.0"
11
11
  },
12
12
  "peerDependencies": {
13
13
  "@kuralle-agents/core": ">=0.8.0"
@@ -8,6 +8,7 @@ import {
8
8
  reconcileSpokenPrefix,
9
9
  rewriteLastAssistant,
10
10
  type KuralleMessageLike,
11
+ type KuralleRunOptions,
11
12
  type KuralleRuntimeLike,
12
13
  type KuralleSessionStoreLike,
13
14
  type KuralleStoredSession,
@@ -74,6 +75,58 @@ async function collectParts(reasoner: Reasoner, turn: ReasonerTurn): Promise<Rea
74
75
  return collected;
75
76
  }
76
77
 
78
+ describe("fromKuralleRuntime G4 resume-by-seed (historyDelta)", () => {
79
+ const turnWithContext = (): ReasonerTurn => ({
80
+ userText: "Second question",
81
+ messages: [
82
+ { role: "system", content: "sys" },
83
+ { role: "user", content: "First question" },
84
+ { role: "assistant", content: "First answer" },
85
+ ],
86
+ signal: new AbortController().signal,
87
+ });
88
+
89
+ it("seeds prior turn.messages into an EMPTY kuralle session (fresh isolate resume)", async () => {
90
+ const spy: { runOpts?: KuralleRunOptions } = {};
91
+ const runtime: KuralleRuntimeLike = {
92
+ run(opts) {
93
+ spy.runOpts = opts;
94
+ return { events: partsToEvents([textDelta("ok"), done()]) };
95
+ },
96
+ getSession: async () => null,
97
+ };
98
+ const reasoner = fromKuralleRuntime(runtime, { sessionId: "sess-1" });
99
+
100
+ await collectParts(reasoner, turnWithContext());
101
+
102
+ expect(spy.runOpts?.input).toBe("Second question");
103
+ expect(spy.runOpts?.historyDelta).toEqual([
104
+ { role: "user", content: "First question" },
105
+ { role: "assistant", content: "First answer" },
106
+ ]);
107
+ });
108
+
109
+ it("does NOT seed into a non-empty session (no double-applied history, R6)", async () => {
110
+ const spy: { runOpts?: KuralleRunOptions } = {};
111
+ const runtime: KuralleRuntimeLike = {
112
+ run(opts) {
113
+ spy.runOpts = opts;
114
+ return { events: partsToEvents([textDelta("ok"), done()]) };
115
+ },
116
+ getSession: async () => ({
117
+ id: "sess-1",
118
+ messages: [{ role: "user", content: "already there" }],
119
+ }),
120
+ };
121
+ const reasoner = fromKuralleRuntime(runtime, { sessionId: "sess-1" });
122
+
123
+ await collectParts(reasoner, turnWithContext());
124
+
125
+ expect(spy.runOpts?.input).toBe("Second question");
126
+ expect(spy.runOpts?.historyDelta).toBeUndefined();
127
+ });
128
+ });
129
+
77
130
  describe("fromKuralleRuntime", () => {
78
131
  it("maps happy path: text deltas and done to finish:stop", async () => {
79
132
  const reasoner = fromKuralleRuntime(
@@ -87,7 +87,23 @@ async function buildKuralleRunOptions(
87
87
  const appended = await appendFlowResumeUserMessage(runtime, opts.sessionId, turn.userText);
88
88
  if (appended) return base;
89
89
  }
90
- return { ...base, input: turn.userText };
90
+ // G4 resume-by-seed: the BRIDGE owns history (reasoner seam §4.5). When the kuralle
91
+ // session is empty (fresh isolate / post-eviction) and the turn carries prior context,
92
+ // seed it once via historyDelta — kuralle appends it to the (empty) session, so the
93
+ // runtime resumes with the same context instead of restarting amnesiac. Never seeded
94
+ // into a non-empty session (that would double-apply history).
95
+ return { ...base, input: turn.userText, ...seedHistoryDelta(session, turn.messages) };
96
+ }
97
+
98
+ function seedHistoryDelta(
99
+ session: KuralleStoredSession | null,
100
+ messages: ReasonerTurn["messages"],
101
+ ): Pick<KuralleRunOptions, "historyDelta"> | Record<string, never> {
102
+ if (session && session.messages.length > 0) return {};
103
+ const delta = messages
104
+ .filter((message) => message.role === "user" || message.role === "assistant")
105
+ .map((message) => ({ role: message.role, content: message.content }));
106
+ return delta.length > 0 ? { historyDelta: delta } : {};
91
107
  }
92
108
 
93
109
  export async function buildKuralleTurnRunOptions(