dsh-local-ai 0.1.1 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.2] - 2026-08-20
9
+
10
+ ### Fixed
11
+
12
+ - Tool calls are emitted with a minted `CallId` instead of an empty one. Ollama's `/api/chat` wire format carries no tool-call id, so `OpenBlock.callId` was never assigned and both the `tool-call-delta` chunks and the closed block ended up as `CallId('')`. Live turns still paired the call with its result positionally, but the persisted `tool/result` carried an empty `source.callId` and the session failed validation on resume (`message must have tool source`).
13
+ - A tool-call array slot reused for a second, unrelated call now opens its own block instead of being diffed as a continuation. Ollama can reuse the same slot across chunks, and the longest-common-prefix diff previously emitted a fragment that reconstructed into invalid JSON while collapsing the two calls into one block.
14
+
8
15
  ## [0.1.1] - 2026-08-19
9
16
 
10
17
  ### Fixed
package/lib/index.js CHANGED
@@ -590,6 +590,18 @@ function closeBlock(block) {
590
590
  }
591
591
  }
592
592
  /**
593
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
594
+ * wire format carries no tool-call id, so one must be synthesized: without it
595
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
596
+ * and the session fails validation on resume. The block index keeps
597
+ * same-millisecond parallel calls apart.
598
+ * @param block - the freshly opened tool-call block.
599
+ * @returns the minted id.
600
+ */
601
+ function mintCallId(block) {
602
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`;
603
+ }
604
+ /**
593
605
  * Compute the append delta from a cumulative JSON string, so the harness's
594
606
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
595
607
  * the arguments object monotonically, so the delta is everything past the
@@ -605,6 +617,36 @@ function argumentsDelta(previous, next) {
605
617
  return next.slice(index);
606
618
  }
607
619
  /**
620
+ * Whether the previous cumulative arguments object is a key/value subset of
621
+ * the next one, i.e. one call grew monotonically. While one call streams,
622
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
623
+ * reappear with different values, so the subset check fails and the slot must
624
+ * open a fresh block. Non-object arguments (or either side failing to parse,
625
+ * which cannot happen for JSON.stringify output but keeps the guard total)
626
+ * fall back to treating the chunk as a continuation.
627
+ * @param previous - the previously seen cumulative JSON (or `''`).
628
+ * @param next - the new cumulative JSON.
629
+ * @returns `true` when `next` extends `previous` monotonically.
630
+ */
631
+ function isMonotonicExtension(previous, next) {
632
+ if (previous === "") return true;
633
+ let prevObj;
634
+ let nextObj;
635
+ try {
636
+ prevObj = JSON.parse(previous);
637
+ nextObj = JSON.parse(next);
638
+ } catch {
639
+ return true;
640
+ }
641
+ if (typeof prevObj !== "object" || prevObj === null || Array.isArray(prevObj)) return true;
642
+ if (typeof nextObj !== "object" || nextObj === null || Array.isArray(nextObj)) return true;
643
+ for (const [key, value] of Object.entries(prevObj)) {
644
+ if (!(key in nextObj)) return false;
645
+ if (JSON.stringify(nextObj[key]) !== JSON.stringify(value)) return false;
646
+ }
647
+ return true;
648
+ }
649
+ /**
608
650
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
609
651
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
610
652
  * arguments; `block-end`, `usage`, and `finish` are deferred to the `done`
@@ -680,6 +722,7 @@ async function* translate(lines) {
680
722
  let block = toolBlocks.get(callIndex);
681
723
  if (!block) {
682
724
  block = open("tool-call");
725
+ block.callId = mintCallId(block);
683
726
  toolBlocks.set(callIndex, block);
684
727
  toolArguments.set(callIndex, "");
685
728
  yield {
@@ -692,7 +735,19 @@ async function* translate(lines) {
692
735
  const cumulative = JSON.stringify(call?.function?.arguments ?? {});
693
736
  const previous = toolArguments.get(callIndex) ?? "";
694
737
  if (cumulative !== previous) {
695
- const fragment = argumentsDelta(previous, cumulative);
738
+ if (!isMonotonicExtension(previous, cumulative)) {
739
+ block = open("tool-call");
740
+ block.callId = mintCallId(block);
741
+ if (call?.function?.name !== void 0) block.name = call.function.name;
742
+ toolBlocks.set(callIndex, block);
743
+ toolArguments.set(callIndex, "");
744
+ yield {
745
+ type: "block-start",
746
+ index: block.index,
747
+ blockType: "tool-call"
748
+ };
749
+ }
750
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? "", cumulative);
696
751
  toolArguments.set(callIndex, cumulative);
697
752
  block.text += fragment;
698
753
  yield {
@@ -1101,7 +1156,7 @@ async function checkHealth(baseURL, fetchImpl, subprocess, requestTimeoutMs, gra
1101
1156
  * The `dsh-local-ai` plugin version. The release script bumps this string
1102
1157
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
1103
1158
  */
1104
- const VERSION = "0.1.1";
1159
+ const VERSION = "0.1.2";
1105
1160
  //#endregion
1106
1161
  //#region src/index.ts
1107
1162
  const name = "local-ai";
@@ -44,6 +44,19 @@ export declare function mapFinishReason(reason: string): FinishReason;
44
44
  * @returns the fragment to append.
45
45
  */
46
46
  export declare function argumentsDelta(previous: string, next: string): string;
47
+ /**
48
+ * Whether the previous cumulative arguments object is a key/value subset of
49
+ * the next one, i.e. one call grew monotonically. While one call streams,
50
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
51
+ * reappear with different values, so the subset check fails and the slot must
52
+ * open a fresh block. Non-object arguments (or either side failing to parse,
53
+ * which cannot happen for JSON.stringify output but keeps the guard total)
54
+ * fall back to treating the chunk as a continuation.
55
+ * @param previous - the previously seen cumulative JSON (or `''`).
56
+ * @param next - the new cumulative JSON.
57
+ * @returns `true` when `next` extends `previous` monotonically.
58
+ */
59
+ export declare function isMonotonicExtension(previous: string, next: string): boolean;
47
60
  /**
48
61
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
49
62
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAgB,YAAY,EAAE,WAAW,EAAc,MAAM,sBAAsB,CAAA;AAE/F,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC,CAAA;KAC1F,CAAA;IACD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAW5D;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED;;;;;;;;GAQG;AACH,wBAAuB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CA4G1F"}
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAgB,YAAY,EAAE,WAAW,EAAc,MAAM,sBAAsB,CAAA;AAE/F,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC,CAAA;KAC1F,CAAA;IACD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAYD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAW5D;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAiB5E;AAED;;;;;;;;GAQG;AACH,wBAAuB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CA0H1F"}
@@ -38,6 +38,18 @@ function closeBlock(block) {
38
38
  };
39
39
  }
40
40
  }
41
+ /**
42
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
43
+ * wire format carries no tool-call id, so one must be synthesized: without it
44
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
45
+ * and the session fails validation on resume. The block index keeps
46
+ * same-millisecond parallel calls apart.
47
+ * @param block - the freshly opened tool-call block.
48
+ * @returns the minted id.
49
+ */
50
+ function mintCallId(block) {
51
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`;
52
+ }
41
53
  /**
42
54
  * Compute the append delta from a cumulative JSON string, so the harness's
43
55
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
@@ -55,6 +67,42 @@ export function argumentsDelta(previous, next) {
55
67
  index += 1;
56
68
  return next.slice(index);
57
69
  }
70
+ /**
71
+ * Whether the previous cumulative arguments object is a key/value subset of
72
+ * the next one, i.e. one call grew monotonically. While one call streams,
73
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
74
+ * reappear with different values, so the subset check fails and the slot must
75
+ * open a fresh block. Non-object arguments (or either side failing to parse,
76
+ * which cannot happen for JSON.stringify output but keeps the guard total)
77
+ * fall back to treating the chunk as a continuation.
78
+ * @param previous - the previously seen cumulative JSON (or `''`).
79
+ * @param next - the new cumulative JSON.
80
+ * @returns `true` when `next` extends `previous` monotonically.
81
+ */
82
+ export function isMonotonicExtension(previous, next) {
83
+ if (previous === '')
84
+ return true;
85
+ let prevObj;
86
+ let nextObj;
87
+ try {
88
+ prevObj = JSON.parse(previous);
89
+ nextObj = JSON.parse(next);
90
+ }
91
+ catch {
92
+ return true;
93
+ }
94
+ if (typeof prevObj !== 'object' || prevObj === null || Array.isArray(prevObj))
95
+ return true;
96
+ if (typeof nextObj !== 'object' || nextObj === null || Array.isArray(nextObj))
97
+ return true;
98
+ for (const [key, value] of Object.entries(prevObj)) {
99
+ if (!(key in nextObj))
100
+ return false;
101
+ if (JSON.stringify(nextObj[key]) !== JSON.stringify(value))
102
+ return false;
103
+ }
104
+ return true;
105
+ }
58
106
  /**
59
107
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
60
108
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -115,6 +163,7 @@ export async function* translate(lines) {
115
163
  let block = toolBlocks.get(callIndex);
116
164
  if (!block) {
117
165
  block = open('tool-call');
166
+ block.callId = mintCallId(block);
118
167
  toolBlocks.set(callIndex, block);
119
168
  toolArguments.set(callIndex, '');
120
169
  yield { type: 'block-start', index: block.index, blockType: 'tool-call' };
@@ -125,7 +174,21 @@ export async function* translate(lines) {
125
174
  const cumulative = JSON.stringify(call?.function?.arguments ?? {});
126
175
  const previous = toolArguments.get(callIndex) ?? '';
127
176
  if (cumulative !== previous) {
128
- const fragment = argumentsDelta(previous, cumulative);
177
+ if (!isMonotonicExtension(previous, cumulative)) {
178
+ // The cumulative arguments no longer extend the ones this slot has
179
+ // seen: Ollama reused the array slot for a second, unrelated call.
180
+ // Diffing against the old call would emit a fragment that
181
+ // reconstructs into invalid JSON and collapse the two calls into
182
+ // one block, so open a fresh block for the new call instead.
183
+ block = open('tool-call');
184
+ block.callId = mintCallId(block);
185
+ if (call?.function?.name !== undefined)
186
+ block.name = call.function.name;
187
+ toolBlocks.set(callIndex, block);
188
+ toolArguments.set(callIndex, '');
189
+ yield { type: 'block-start', index: block.index, blockType: 'tool-call' };
190
+ }
191
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? '', cumulative);
129
192
  toolArguments.set(callIndex, cumulative);
130
193
  block.text += fragment;
131
194
  yield {
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/translate.ts"],"names":[],"mappings":"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"}
@@ -2,5 +2,5 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export declare const VERSION = "0.1.1";
5
+ export declare const VERSION = "0.1.2";
6
6
  //# sourceMappingURL=version.d.ts.map
@@ -2,5 +2,5 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export const VERSION = '0.1.1';
5
+ export const VERSION = '0.1.2';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-local-ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local-model (Ollama) integration for DeepSeek Harness: discover, pull, remove, and inspect local models, route requests to them by task type or keyword with automatic fallback to the cloud, and get a one-shot status overview via /ollama.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/translate.ts CHANGED
@@ -69,6 +69,19 @@ function closeBlock(block: OpenBlock): ContentBlock {
69
69
  }
70
70
  }
71
71
 
72
+ /**
73
+ * Mint a session-unique tool-call id for a freshly opened block. Ollama's
74
+ * wire format carries no tool-call id, so one must be synthesized: without it
75
+ * the emitted chunks and the persisted tool result all carry an empty CallId,
76
+ * and the session fails validation on resume. The block index keeps
77
+ * same-millisecond parallel calls apart.
78
+ * @param block - the freshly opened tool-call block.
79
+ * @returns the minted id.
80
+ */
81
+ function mintCallId(block: OpenBlock): string {
82
+ return `ollama-${Date.now().toString(36)}-${block.index}-${Math.random().toString(36).slice(2, 8)}`
83
+ }
84
+
72
85
  /**
73
86
  * Compute the append delta from a cumulative JSON string, so the harness's
74
87
  * delta-concatenating assembler reconstructs the full arguments. Ollama grows
@@ -85,6 +98,37 @@ export function argumentsDelta(previous: string, next: string): string {
85
98
  return next.slice(index)
86
99
  }
87
100
 
101
+ /**
102
+ * Whether the previous cumulative arguments object is a key/value subset of
103
+ * the next one, i.e. one call grew monotonically. While one call streams,
104
+ * Ollama only adds keys; when it reuses a slot for a new call, existing keys
105
+ * reappear with different values, so the subset check fails and the slot must
106
+ * open a fresh block. Non-object arguments (or either side failing to parse,
107
+ * which cannot happen for JSON.stringify output but keeps the guard total)
108
+ * fall back to treating the chunk as a continuation.
109
+ * @param previous - the previously seen cumulative JSON (or `''`).
110
+ * @param next - the new cumulative JSON.
111
+ * @returns `true` when `next` extends `previous` monotonically.
112
+ */
113
+ export function isMonotonicExtension(previous: string, next: string): boolean {
114
+ if (previous === '') return true
115
+ let prevObj: unknown
116
+ let nextObj: unknown
117
+ try {
118
+ prevObj = JSON.parse(previous)
119
+ nextObj = JSON.parse(next)
120
+ } catch {
121
+ return true
122
+ }
123
+ if (typeof prevObj !== 'object' || prevObj === null || Array.isArray(prevObj)) return true
124
+ if (typeof nextObj !== 'object' || nextObj === null || Array.isArray(nextObj)) return true
125
+ for (const [key, value] of Object.entries(prevObj)) {
126
+ if (!(key in nextObj)) return false
127
+ if (JSON.stringify((nextObj as Record<string, unknown>)[key]) !== JSON.stringify(value)) return false
128
+ }
129
+ return true
130
+ }
131
+
88
132
  /**
89
133
  * Consume Ollama NDJSON lines and yield StreamChunks. Text and reasoning deltas
90
134
  * stream as they arrive; tool-call deltas are diffed from the cumulative wire
@@ -150,6 +194,7 @@ export async function* translate(lines: AsyncIterable<string>): AsyncGenerator<S
150
194
  let block = toolBlocks.get(callIndex)
151
195
  if (!block) {
152
196
  block = open('tool-call')
197
+ block.callId = mintCallId(block)
153
198
  toolBlocks.set(callIndex, block)
154
199
  toolArguments.set(callIndex, '')
155
200
  yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
@@ -160,7 +205,20 @@ export async function* translate(lines: AsyncIterable<string>): AsyncGenerator<S
160
205
  const cumulative = JSON.stringify(call?.function?.arguments ?? {})
161
206
  const previous = toolArguments.get(callIndex) ?? ''
162
207
  if (cumulative !== previous) {
163
- const fragment = argumentsDelta(previous, cumulative)
208
+ if (!isMonotonicExtension(previous, cumulative)) {
209
+ // The cumulative arguments no longer extend the ones this slot has
210
+ // seen: Ollama reused the array slot for a second, unrelated call.
211
+ // Diffing against the old call would emit a fragment that
212
+ // reconstructs into invalid JSON and collapse the two calls into
213
+ // one block, so open a fresh block for the new call instead.
214
+ block = open('tool-call')
215
+ block.callId = mintCallId(block)
216
+ if (call?.function?.name !== undefined) block.name = call.function.name
217
+ toolBlocks.set(callIndex, block)
218
+ toolArguments.set(callIndex, '')
219
+ yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
220
+ }
221
+ const fragment = argumentsDelta(toolArguments.get(callIndex) ?? '', cumulative)
164
222
  toolArguments.set(callIndex, cumulative)
165
223
  block.text += fragment
166
224
  yield {
package/src/version.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * The `dsh-local-ai` plugin version. The release script bumps this string
3
3
  * alongside `package.json`; `test/version.spec.ts` trips when the two drift.
4
4
  */
5
- export const VERSION = '0.1.1'
5
+ export const VERSION = '0.1.2'