@7n/test 0.10.0 → 0.10.1

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1] - 2026-07-02
4
+
5
+ ### Fixed
6
+
7
+ - callText і callAgent (pi-client) тепер ретраять transient-помилки з'єднання (напр. до локального omlx-сервера під навантаженням) з експоненційним backoff + jitter замість негайного провалу файлу; кількість спроб і базова затримка налаштовуються через N_PI_RETRY_ATTEMPTS / N_PI_RETRY_DELAY_MS.
8
+
3
9
  ## [0.10.0] - 2026-07-02
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: pi-client.mjs
4
4
  resource: npm/src/lib/pi-client.mjs
5
5
  docgen:
6
- crc: 4649ce83
6
+ crc: f2c45cdc
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -5,20 +5,54 @@
5
5
  * Two modes:
6
6
  * callText(prompt, model?) — text-only, no tools, returns string
7
7
  * callAgent(prompt, cwd) — coding tools enabled, writes files directly
8
+ *
9
+ * Both retry transient connection failures (e.g. a shared local model server
10
+ * that's momentarily busy under concurrent load) with exponential backoff
11
+ * instead of failing the caller's file on the first hiccup.
8
12
  */
9
13
  import { createAgentSession, SessionManager, ModelRegistry, AuthStorage } from '@earendil-works/pi-coding-agent'
14
+ import { env } from 'node:process'
15
+ import { setTimeout as sleep } from 'node:timers/promises'
10
16
 
11
17
  let _registry = null
18
+ /**
19
+ *
20
+ */
12
21
  async function getRegistry() {
13
22
  if (_registry) return _registry
14
23
  _registry = ModelRegistry.create(AuthStorage.create())
15
24
  return _registry
16
25
  }
17
26
 
27
+ const RETRYABLE_ERROR_RE = /connection error|ECONNREFUSED|ETIMEDOUT|fetch failed|network/i
28
+ const MAX_ATTEMPTS = Number(env.N_PI_RETRY_ATTEMPTS) || 4
29
+ const BASE_DELAY_MS = Number(env.N_PI_RETRY_DELAY_MS) || 1500
30
+ const MAX_DELAY_MS = 15_000
31
+
32
+ /**
33
+ * Retries `fn` with exponential backoff + jitter when it throws a transient
34
+ * connection-ish error (shared local model server busy/restarting); other
35
+ * errors (auth, malformed request) are re-thrown immediately.
36
+ * @template T
37
+ * @param {() => Promise<T>} fn operation to retry
38
+ * @returns {Promise<T>} result of the first successful attempt
39
+ */
40
+ async function withRetry(fn) {
41
+ for (let attempt = 1; ; attempt++) {
42
+ try {
43
+ return await fn()
44
+ } catch (error) {
45
+ if (attempt >= MAX_ATTEMPTS || !RETRYABLE_ERROR_RE.test(error.message ?? '')) throw error
46
+ const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
47
+ const jitter = delay * (0.5 + Math.random() * 0.5)
48
+ await sleep(jitter)
49
+ }
50
+ }
51
+ }
52
+
18
53
  /**
19
54
  * Sends a single prompt to pi in text mode (no tools) and returns the response.
20
55
  * Reads auth/model config from ~/.pi/ same as the CLI.
21
- *
22
56
  * @param {string} prompt
23
57
  * @param {object} [opts]
24
58
  * @param {string} [opts.cwd]
@@ -26,51 +60,54 @@ async function getRegistry() {
26
60
  * @returns {Promise<string>}
27
61
  */
28
62
  export async function callText(prompt, opts = {}) {
29
- const cwd = opts.cwd ?? process.cwd()
30
- const sessionOpts = {
31
- tools: [],
32
- sessionManager: SessionManager.inMemory(cwd),
33
- cwd
34
- }
35
- if (opts.model) {
36
- const registry = await getRegistry()
37
- const slashIdx = opts.model.indexOf('/')
38
- const provider = slashIdx >= 0 ? opts.model.slice(0, slashIdx) : null
39
- const modelId = slashIdx >= 0 ? opts.model.slice(slashIdx + 1) : opts.model
40
- const resolved = provider ? registry.find(provider, modelId) : null
41
- sessionOpts.modelRegistry = registry
42
- sessionOpts.model = resolved ?? opts.model
43
- }
44
- const { session } = await createAgentSession(sessionOpts)
63
+ return withRetry(async () => {
64
+ const cwd = opts.cwd ?? process.cwd()
65
+ const sessionOpts = {
66
+ tools: [],
67
+ sessionManager: SessionManager.inMemory(cwd),
68
+ cwd
69
+ }
70
+ if (opts.model) {
71
+ const registry = await getRegistry()
72
+ const slashIdx = opts.model.indexOf('/')
73
+ const provider = slashIdx === -1 ? null : opts.model.slice(0, slashIdx)
74
+ const modelId = slashIdx === -1 ? opts.model : opts.model.slice(slashIdx + 1)
75
+ const resolved = provider ? registry.find(provider, modelId) : null
76
+ sessionOpts.modelRegistry = registry
77
+ sessionOpts.model = resolved ?? opts.model
78
+ }
79
+ const { session } = await createAgentSession(sessionOpts)
45
80
 
46
- await session.prompt(prompt)
81
+ await session.prompt(prompt)
47
82
 
48
- const state = session.state
49
- const last = state.messages[state.messages.length - 1]
50
- if (!last || last.role !== 'assistant') return ''
51
- if (last.stopReason === 'error' || last.stopReason === 'aborted') {
52
- throw new Error(`pi error: ${last.errorMessage ?? last.stopReason}`)
53
- }
54
- return last.content
55
- .filter(c => c.type === 'text')
56
- .map(c => c.text)
57
- .join('')
83
+ const state = session.state
84
+ const last = state.messages.at(-1)
85
+ if (!last || last.role !== 'assistant') return ''
86
+ if (last.stopReason === 'error' || last.stopReason === 'aborted') {
87
+ throw new Error(`pi error: ${last.errorMessage ?? last.stopReason}`)
88
+ }
89
+ return last.content
90
+ .filter(c => c.type === 'text')
91
+ .map(c => c.text)
92
+ .join('')
93
+ })
58
94
  }
59
95
 
60
96
  /**
61
97
  * Sends a prompt to pi in agent mode with full coding tools (read/write/bash/edit).
62
98
  * The agent writes test files directly — no need to parse output.
63
- *
64
99
  * @param {string} prompt
65
100
  * @param {string} cwd project root where files should be written
66
101
  * @returns {Promise<void>}
67
102
  */
68
103
  export async function callAgent(prompt, cwd) {
69
- const { session } = await createAgentSession({
70
- tools: ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls'],
71
- sessionManager: SessionManager.inMemory(cwd),
72
- cwd
73
- })
104
+ return withRetry(async () => {
105
+ const { session } = await createAgentSession({
106
+ tools: ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls'],
107
+ sessionManager: SessionManager.inMemory(cwd),
108
+ cwd
109
+ })
74
110
 
75
- await session.prompt(prompt)
111
+ await session.prompt(prompt)
112
+ })
76
113
  }