@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 +6 -0
- package/package.json +1 -1
- package/src/lib/docs/pi-client.md +1 -1
- package/src/lib/pi-client.mjs +72 -35
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
package/src/lib/pi-client.mjs
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
81
|
+
await session.prompt(prompt)
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
111
|
+
await session.prompt(prompt)
|
|
112
|
+
})
|
|
76
113
|
}
|