deepseek-coordination 0.1.1 → 0.1.5

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.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * cache-measure.mjs — measure the prefix-cache effect of this plugin's design decisions, on the real endpoint.
3
+ *
4
+ * THE MISTAKE THIS VERSION FIXES
5
+ *
6
+ * The first version salted each variant's SYSTEM PROMPT to give it its own cache lineage, and then mutated
7
+ * the system prompt. That compares a mutated prompt against a baseline that was never the same bytes, so
8
+ * every reading came back at ~96% and the measurement was worthless. The lineage identity and the thing
9
+ * being mutated have to be independent, and here they are: one shared, fixed system prompt and tool
10
+ * catalog, and the lineage distinguished by a long conversation prefix.
11
+ *
12
+ * METHOD
13
+ *
14
+ * Four users are separated by >1800 tokens of distinct filler, which exceeds the interval at which the
15
+ * provider carves cache prefix units, so user B's requests cannot match user A's units or each other's
16
+ * variants. Each reading is ONE request after a warm request on the same lineage — never two identical
17
+ * requests, which would persist a longer unit and hide the variant's true cost.
18
+ *
19
+ * `max_tokens: 1` keeps requests cheap; the cache prefix is settled before decoding, so the cap does not
20
+ * affect what is being measured.
21
+ *
22
+ * Usage: node .temporary/cache-measure.mjs
23
+ * The key is read from $DSH_HOME/.credentials.yaml and never printed.
24
+ */
25
+
26
+ import { readFileSync } from 'node:fs'
27
+ import { join } from 'node:path'
28
+
29
+ const DSH_HOME = process.env.DSH_HOME ?? 'C:/Users/Administrator/.dsh'
30
+ const MODEL = 'deepseek-flash'
31
+ const ENDPOINT = 'https://api.deepseek.com/anthropic/v1/messages'
32
+
33
+ function apiKey() {
34
+ const text = readFileSync(join(DSH_HOME, '.credentials.yaml'), 'utf8')
35
+ const line = text.split(/\r?\n/).find((l) => l.trim().startsWith('DEEPSEEK_API_KEY:'))
36
+ if (line === undefined) throw new Error('DEEPSEEK_API_KEY is not present in .credentials.yaml')
37
+ const value = line.split(':').slice(1).join(':').trim()
38
+ if (value === '') throw new Error('DEEPSEEK_API_KEY is empty')
39
+ return value
40
+ }
41
+ const KEY = apiKey()
42
+
43
+ const FILLER = 'The quick brown fox jumps over the lazy dog and then documents the boundary conditions. '
44
+ /** Filler whose tokens differ per word, so two users' prefixes cannot coincide. */
45
+ function filler(word, blocks = 260) {
46
+ return Array.from({ length: blocks }, (_, i) => `${i + 1}. ${word} ${FILLER}`).join('\n')
47
+ }
48
+
49
+ const BASE_SYSTEM = 'You are an AI agent powered by DeepSeek Harness. Follow the user instruction exactly.'
50
+ const TOOLS = [
51
+ { name: 'read', description: 'Read a UTF-8 text file and return line-numbered content.', input_schema: { type: 'object', properties: { file_path: { type: 'string' } }, required: ['file_path'] } },
52
+ { name: 'write', description: 'Create or fully replace a UTF-8 text file.', input_schema: { type: 'object', properties: { file_path: { type: 'string' }, content: { type: 'string' } }, required: ['file_path', 'content'] } },
53
+ { name: 'pwsh', description: 'Execute a PowerShell command and return its stdout/stderr.', input_schema: { type: 'object', properties: { command: { type: 'string' }, description: { type: 'string' } }, required: ['command', 'description'] } },
54
+ { name: 'todo_write', description: 'Record and update a task list to plan multi-step work and show progress.', input_schema: { type: 'object', properties: { todos: { type: 'array', items: { type: 'object' } } }, required: ['todos'] } },
55
+ { name: 'workflow_plan', description: 'Intent-first workflow: agree on what is wanted and how it will be done, before doing it.', input_schema: { type: 'object', properties: { action: { type: 'string' } }, required: ['action'] } },
56
+ ]
57
+
58
+ async function call({ system = BASE_SYSTEM, messages, tools = TOOLS }) {
59
+ const res = await fetch(ENDPOINT, {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json', 'x-api-key': KEY, 'anthropic-version': '2023-06-01' },
62
+ body: JSON.stringify({ model: MODEL, max_tokens: 1, system, messages, tools }),
63
+ })
64
+ const text = await res.text()
65
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`)
66
+ const json = JSON.parse(text)
67
+ const u = json.usage ?? {}
68
+ const input = u.input_tokens ?? 0
69
+ const cacheRead = u.cache_read_input_tokens ?? 0
70
+ const cacheWrite = u.cache_creation_input_tokens ?? 0
71
+ const total = input + cacheRead + cacheWrite
72
+ return { input, cacheRead, cacheWrite, total, rate: total === 0 ? 0 : cacheRead / total, content: json.content ?? [] }
73
+ }
74
+
75
+ /**
76
+ * A tool-call turn, built by hand.
77
+ *
78
+ * Thinking mode rejects `tool_choice` outright ("Thinking mode does not support this tool_choice") and
79
+ * requires every assistant turn it reads back to carry a `thinking` block, so neither a plain assistant
80
+ * message nor a forced tool call works here. An empty thinking block beside the `tool_use` is accepted,
81
+ * which keeps this measurement to ONE request per variant instead of paying for a model call to author
82
+ * each turn. `max_tokens: 1` is correct for the same reason: the cache prefix is settled before decoding,
83
+ * and the reply is not read.
84
+ */
85
+ function toolTurn(index, action, result) {
86
+ return [
87
+ {
88
+ role: 'assistant',
89
+ content: [
90
+ { type: 'thinking', thinking: '', signature: '' },
91
+ { type: 'tool_use', id: `t${index}`, name: 'workflow_plan', input: { action } },
92
+ ],
93
+ },
94
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: `t${index}`, content: result }] },
95
+ ]
96
+ }
97
+
98
+ const rows = []
99
+ function report(name, usage) {
100
+ rows.push({ name, ...usage })
101
+ console.log(` ${name.padEnd(42)} read=${String(usage.cacheRead).padStart(6)} write=${String(usage.cacheWrite).padStart(6)} miss=${String(usage.input).padStart(5)} total=${String(usage.total).padStart(6)} hit=${(usage.rate * 100).toFixed(1)}%`)
102
+ return usage
103
+ }
104
+
105
+ /** Warm one lineage once, then send the variant once. */
106
+ async function variant(user, name, mutate) {
107
+ const base = { messages: [{ role: 'user', content: `${filler(user)}\n\nSummarise line 1 in one word.` }] }
108
+ await call(base)
109
+ return report(name, await call(mutate(base)))
110
+ }
111
+
112
+ console.log(`prefix-cache measurement — ${ENDPOINT} — model ${MODEL}`)
113
+ console.log('one request per variant, after one warm request on the same lineage\n')
114
+
115
+ console.log('A user: the design this plugin uses — nothing touches the system prompt or the tool catalog')
116
+ await variant('alpha', 'A1 unchanged (control)', (r) => r)
117
+ await variant('alpha', 'A2 append a user message', (r) => ({
118
+ messages: [...r.messages, { role: 'user', content: 'Now say OK.' }],
119
+ }))
120
+ await variant('alpha', 'A3 append a tool result', (r) => ({
121
+ messages: [...r.messages, ...toolTurn('a3', 'start', '{"ok":true,"phase":"plan","next":"call workflow_plan with action \\"plan\\""}')],
122
+ }))
123
+
124
+ console.log('\nB user: the rejected design — the phase rendered into the system prompt')
125
+ await variant('bravo', 'B1 unchanged (control)', (r) => r)
126
+ await variant('bravo', 'B2 rewrite the LEADING system prompt', () => ({
127
+ messages: [{ role: 'user', content: `${filler('bravo')}\n\nSummarise line 1 in one word.` }],
128
+ system: `${BASE_SYSTEM}\n## Workflow: plan. Call workflow_plan with action "plan".`,
129
+ }))
130
+ await variant('bravo', 'B3 append an in-history system message', () => ({
131
+ messages: [{ role: 'user', content: `${filler('bravo')}\n\nSummarise line 1 in one word.` }],
132
+ system: [
133
+ { type: 'text', text: BASE_SYSTEM },
134
+ { type: 'text', text: '## Workflow: plan. Call workflow_plan with action "plan".' },
135
+ ],
136
+ }))
137
+
138
+ console.log('\nC user: the tool catalog')
139
+ await variant('charlie', 'C1 unchanged (control)', (r) => r)
140
+ await variant('charlie', 'C2 change one tool description', (r) => ({
141
+ messages: r.messages,
142
+ tools: TOOLS.map((t) => (t.name === 'workflow_plan' ? { ...t, description: `${t.description} (edited)` } : t)),
143
+ }))
144
+ await variant('charlie', 'C3 swap two tools', (r) => ({
145
+ messages: r.messages,
146
+ tools: [TOOLS[1], TOOLS[0], ...TOOLS.slice(2)],
147
+ }))
148
+
149
+ console.log('\nD user: one whole workflow, four calls, system prompt and catalog never moving')
150
+ const workflow = await (async () => {
151
+ const messages = [{ role: 'user', content: `${filler('delta')}\n\nSummarise line 1 in one word.` }]
152
+ await call({ messages })
153
+ const steps = []
154
+ const phases = [
155
+ ['start', '{"ok":true,"phase":"plan","next":"call workflow_plan with action \\"plan\\""}'],
156
+ ['plan', '{"ok":true,"phase":"choose","next":"call workflow_plan with action \\"choose\\""}'],
157
+ ['choose', '{"ok":true,"phase":"run","next":"call workflow_plan with action \\"run\\""}'],
158
+ ['run', '{"ok":true,"phase":"done","next":null}'],
159
+ ]
160
+ for (const [index, [action, result]] of phases.entries()) {
161
+ messages.push(...toolTurn(index, action, result))
162
+ const usage = await call({ messages })
163
+ steps.push(usage)
164
+ console.log(` ${String(index + 1)} ${action.padEnd(38)} read=${String(usage.cacheRead).padStart(6)} write=${String(usage.cacheWrite).padStart(6)} miss=${String(usage.input).padStart(5)} total=${String(usage.total).padStart(6)} hit=${(usage.rate * 100).toFixed(1)}%`)
165
+ }
166
+ return steps
167
+ })()
168
+
169
+ console.log('\nE user: the same four phases with the prompt rewritten each time')
170
+ const rewritten = await (async () => {
171
+ const messages = [{ role: 'user', content: `${filler('echo')}\n\nSummarise line 1 in one word.` }]
172
+ await call({ messages })
173
+ const out = []
174
+ for (const phase of ['plan', 'choose', 'run']) {
175
+ const usage = await call({ messages, system: `${BASE_SYSTEM}\n## Workflow: ${phase}.` })
176
+ out.push(usage)
177
+ console.log(` phase -> ${phase.padEnd(34)} read=${String(usage.cacheRead).padStart(6)} write=${String(usage.cacheWrite).padStart(6)} miss=${String(usage.input).padStart(5)} total=${String(usage.total).padStart(6)} hit=${(usage.rate * 100).toFixed(1)}%`)
178
+ }
179
+ return out
180
+ })()
181
+
182
+ // --- verdict -----------------------------------------------------------------------------------
183
+ const mean = (list) => list.reduce((a, u) => a + u.rate, 0) / list.length
184
+ const find = (prefix) => rows.find((r) => r.name.startsWith(prefix))
185
+ const workflowRate = mean(workflow)
186
+ const rewriteRate = mean(rewritten)
187
+ const appendRate = (find('A2').rate + find('A3').rate) / 2
188
+ const inHistory = find('B3').rate
189
+ const leading = find('B2').rate
190
+ const missCost = rewritten.reduce((a, u) => a + u.input, 0)
191
+
192
+ console.log('\n=== results ===')
193
+ console.log(`appending history (the plugin's shape) : ${(appendRate * 100).toFixed(1)}% mean hit`)
194
+ console.log(`one workflow, four calls, appending only : ${(workflowRate * 100).toFixed(1)}% mean hit`)
195
+ console.log(`rewriting the leading system prompt : ${(leading * 100).toFixed(1)}% hit, and ${(rewriteRate * 100).toFixed(1)}% across three phases`)
196
+ console.log(`appending an in-history system message : ${(inHistory * 100).toFixed(1)}% hit`)
197
+ console.log(`changing a tool description : ${(find('C2').rate * 100).toFixed(1)}% hit`)
198
+ console.log(`swapping two tools : ${(find('C3').rate * 100).toFixed(1)}% hit`)
199
+ console.log(`cache reads the four appended calls kept : ${workflow.reduce((a, u) => a + u.cacheRead, 0)} tokens`)
200
+
201
+ /**
202
+ * The claims this experiment is entitled to make.
203
+ *
204
+ * Only the append side. The mutation readings (rewriting the prompt, editing or reordering tools) are NOT
205
+ * asserted here, and that is a deliberate retreat after measuring them twice and getting opposite answers:
206
+ * the first run read 0.0% for all of them, the second read 97%. The provider documents why — it persists a
207
+ * common prefix once it detects one ACROSS requests, so by the second run my own repeated identical
208
+ * prefixes had been learned, and a mutation that changes them still matched. Per-variant A/B isolation on
209
+ * a shared account therefore cannot settle the mutation side; the append side is stable across both runs
210
+ * and is what this plugin's design actually depends on.
211
+ */
212
+ const claims = {
213
+ 'appending history keeps the cache (>= 90%)': appendRate >= 0.9,
214
+ 'a whole workflow of appended calls stays warm (>= 90%)': workflowRate >= 0.9,
215
+ }
216
+ console.log('\n=== claims ===')
217
+ let failed = 0
218
+ for (const [claim, ok] of Object.entries(claims)) {
219
+ console.log(` ${ok ? 'HOLDS ' : 'FAILED '} ${claim}`)
220
+ if (!ok) failed += 1
221
+ }
222
+ console.log(failed === 0 ? '\nRESULT: PASS' : `\nRESULT: FAIL (${failed} claim(s) did not hold)`)
223
+ process.exit(failed === 0 ? 0 : 1)