pi-agy-acp 0.1.0 → 0.1.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/docs/development.md +24 -0
- package/docs/reference.md +11 -1
- package/package.json +1 -1
- package/src/preflight.ts +61 -0
- package/src/runtime.ts +42 -19
package/docs/development.md
CHANGED
|
@@ -70,6 +70,30 @@ To inspect the package contents without publishing:
|
|
|
70
70
|
npm pack --dry-run
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## CI and releases
|
|
74
|
+
|
|
75
|
+
`ci.yml` checks pull requests and pushes to `main`. `release.yml` runs only for
|
|
76
|
+
version-tag pushes or manual runs on an existing tag, so ordinary commits do not
|
|
77
|
+
run a second test suite or trigger publication. Both use Node.js 24. There is no
|
|
78
|
+
committed lockfile, so dependency installation uses `npm install`.
|
|
79
|
+
|
|
80
|
+
To release, bump `package.json`, commit the change, and push its matching
|
|
81
|
+
`vX.Y.Z` tag. The workflow accepts stable versions only; publish one version at
|
|
82
|
+
a time and never move a published tag. It validates the tag and version, then
|
|
83
|
+
checks, tests, and packs before publishing. Only a registry 404 means the version
|
|
84
|
+
is absent; network, authentication, and server errors stop the job.
|
|
85
|
+
|
|
86
|
+
Configure npm [Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)
|
|
87
|
+
for owner `fdkevin0`, repository `pi-agy-acp`, and workflow filename `release.yml`.
|
|
88
|
+
The workflow grants OIDC permission to the publish job and uses npm >= 11.5.1.
|
|
89
|
+
An optional `NPM_TOKEN` repository secret supports token authentication through
|
|
90
|
+
`NODE_AUTH_TOKEN`; use a granular write token with the required 2FA bypass.
|
|
91
|
+
|
|
92
|
+
If npm publication succeeds but GitHub Release creation fails, rerun that tagged
|
|
93
|
+
workflow, or use `gh workflow run release.yml --ref vX.Y.Z`. Branch-based manual
|
|
94
|
+
runs are rejected. Existing npm versions are not republished, and GitHub Releases
|
|
95
|
+
are created only for existing tags with `gh release create --verify-tag`.
|
|
96
|
+
|
|
73
97
|
## Documentation conventions
|
|
74
98
|
|
|
75
99
|
Keep all project documentation in English. Use progressive disclosure:
|
package/docs/reference.md
CHANGED
|
@@ -98,6 +98,14 @@ PI_AGY_COMMAND=/absolute/path/to/agy pi -e ./index.ts
|
|
|
98
98
|
The dedicated directory isolates the bridge agent from your normal agy tools and
|
|
99
99
|
plugins. It contains agy-managed session data, logs, and the extension's model cache.
|
|
100
100
|
|
|
101
|
+
Closed temporary ACP sessions have their database files and brain directories
|
|
102
|
+
removed automatically. Before starting ACP, the extension also removes leftover
|
|
103
|
+
session files, summary indexes, and numeric `acp.log.<pid>` backups. It retains
|
|
104
|
+
the current `acp.log`, model cache, and authentication files; Pi owns the history
|
|
105
|
+
used to rebuild sessions. Cleanup is best-effort if files are locked. Use a
|
|
106
|
+
different `PI_AGY_HOME` for each concurrently running Pi instance, since startup
|
|
107
|
+
cleanup assumes exclusive use of the directory.
|
|
108
|
+
|
|
101
109
|
With file-backed authentication, the extension copies the existing OAuth token
|
|
102
110
|
from your normal agy directory and sets its permissions to `0600`. Do not commit
|
|
103
111
|
or share this directory. Authentication and token refresh remain agy's responsibility.
|
|
@@ -106,7 +114,9 @@ or share this directory. Authentication and token refresh remain agy's responsib
|
|
|
106
114
|
|
|
107
115
|
Loading the extension reads the cached model catalog, or falls back to five model
|
|
108
116
|
families, without starting agy. Pi's model refresh queries ACP for the account's
|
|
109
|
-
catalog
|
|
117
|
+
catalog once per runtime. Concurrent refreshes share one discovery session;
|
|
118
|
+
after a refresh or normal session observes the live catalog, later refreshes
|
|
119
|
+
reuse it. The first request also discovers models and validates the selection.
|
|
110
120
|
Cache-only refreshes and refreshes already cancelled before startup do not launch
|
|
111
121
|
ACP. Server-advertised models are not hidden, and live catalog changes update
|
|
112
122
|
Pi's model selector. The selector exposes only the ACP reasoning levels offered
|
package/package.json
CHANGED
package/src/preflight.ts
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
writeFileSync,
|
|
17
17
|
readFileSync,
|
|
18
18
|
statSync,
|
|
19
|
+
lstatSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
rmSync,
|
|
19
22
|
} from 'node:fs'
|
|
20
23
|
import { homedir } from 'node:os'
|
|
21
24
|
import { delimiter, isAbsolute, join } from 'node:path'
|
|
@@ -208,6 +211,46 @@ export function syncOAuthToken(
|
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
/** Remove only one closed ACP session's files; session ids come from the agent. */
|
|
215
|
+
export function removeSessionArtifacts(home: string, sessionId: string): void {
|
|
216
|
+
if (
|
|
217
|
+
typeof sessionId !== 'string' ||
|
|
218
|
+
sessionId.length === 0 ||
|
|
219
|
+
/[^A-Za-z0-9._-]/.test(sessionId) ||
|
|
220
|
+
sessionId === '.' ||
|
|
221
|
+
sessionId.includes('..')
|
|
222
|
+
)
|
|
223
|
+
return
|
|
224
|
+
const appData = join(home, 'agy')
|
|
225
|
+
if (!isRealDirectory(appData)) return
|
|
226
|
+
removeArtifacts(join(appData, 'conversations'), [
|
|
227
|
+
`${sessionId}.db`,
|
|
228
|
+
`${sessionId}.db-wal`,
|
|
229
|
+
`${sessionId}.db-shm`,
|
|
230
|
+
])
|
|
231
|
+
removeArtifacts(join(appData, 'brain'), [sessionId])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isRealDirectory(path: string): boolean {
|
|
235
|
+
try {
|
|
236
|
+
return lstatSync(path).isDirectory()
|
|
237
|
+
} catch {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function removeArtifacts(directory: string, names: readonly string[]): void {
|
|
243
|
+
// Never follow a replaced parent directory into data outside the isolated home.
|
|
244
|
+
if (!isRealDirectory(directory)) return
|
|
245
|
+
for (const name of names) {
|
|
246
|
+
try {
|
|
247
|
+
rmSync(join(directory, name), { recursive: true, force: true })
|
|
248
|
+
} catch {
|
|
249
|
+
// Locked or unreadable leftovers can be retried on the next startup.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
211
254
|
/**
|
|
212
255
|
* Write the isolated Antigravity home: bridge agent and ACP settings.
|
|
213
256
|
* @param home - absolute home directory used as `--gemini-dir`.
|
|
@@ -240,6 +283,24 @@ export function prepareHome(
|
|
|
240
283
|
)
|
|
241
284
|
const appData = join(home, 'agy')
|
|
242
285
|
mkdirSync(appData, { recursive: true })
|
|
286
|
+
// Pi owns conversation history; no ACP session is restored across processes.
|
|
287
|
+
// shortcut: cleanup requires an exclusive home; add locking before supporting shared homes.
|
|
288
|
+
removeArtifacts(appData, [
|
|
289
|
+
'conversations',
|
|
290
|
+
'brain',
|
|
291
|
+
'conversation_summaries.db',
|
|
292
|
+
'conversation_summaries.db-wal',
|
|
293
|
+
'conversation_summaries.db-shm',
|
|
294
|
+
'jetbox_summaries_proto.pb',
|
|
295
|
+
])
|
|
296
|
+
try {
|
|
297
|
+
removeArtifacts(
|
|
298
|
+
home,
|
|
299
|
+
readdirSync(home).filter((name) => /^acp\.log\.\d+$/.test(name)),
|
|
300
|
+
)
|
|
301
|
+
} catch {
|
|
302
|
+
// Log cleanup is best-effort when the home cannot be listed.
|
|
303
|
+
}
|
|
243
304
|
syncOAuthToken(home, userHome)
|
|
244
305
|
const settingsPath = join(appData, 'settings.json')
|
|
245
306
|
const settings = readJson(settingsPath)
|
package/src/runtime.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
GPT_OSS_CONTEXT_WINDOW,
|
|
68
68
|
DEFAULT_CONTEXT_WINDOW,
|
|
69
69
|
prepareHome,
|
|
70
|
+
removeSessionArtifacts,
|
|
70
71
|
readVersion,
|
|
71
72
|
resolveAgyBinary,
|
|
72
73
|
syncOAuthToken,
|
|
@@ -184,6 +185,7 @@ export class AntigravityRuntime {
|
|
|
184
185
|
private state: RuntimeStatus['state'] = 'idle'
|
|
185
186
|
private lastError: string | undefined
|
|
186
187
|
private tierDiscovery: Promise<void> | undefined
|
|
188
|
+
private warmupPromise: Promise<void> | undefined
|
|
187
189
|
private readonly conversations = new Map<string, Conversation>()
|
|
188
190
|
private readonly tenants = new Map<string, Conversation>()
|
|
189
191
|
private readonly mutex = new KeyedMutex()
|
|
@@ -344,24 +346,39 @@ export class AntigravityRuntime {
|
|
|
344
346
|
* @returns resolves when discovery finished.
|
|
345
347
|
*/
|
|
346
348
|
async warmup(): Promise<void> {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
result.models?.availableModels,
|
|
360
|
-
result._meta?.antigravity?.tier,
|
|
349
|
+
if (this.warmupPromise !== undefined) return this.warmupPromise
|
|
350
|
+
if (this.catalog.observed) return
|
|
351
|
+
this.warmupPromise = (async () => {
|
|
352
|
+
const proc = await this.ensureReady()
|
|
353
|
+
if (this.catalog.observed) return
|
|
354
|
+
const result = await proc.newSession(
|
|
355
|
+
{
|
|
356
|
+
cwd: this.preflight?.workspace ?? this.home,
|
|
357
|
+
mcpServers: [],
|
|
358
|
+
},
|
|
359
|
+
{ onUpdate: () => undefined },
|
|
360
|
+
120_000,
|
|
361
361
|
)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
try {
|
|
363
|
+
if (
|
|
364
|
+
this.catalog.observe(
|
|
365
|
+
result.configOptions,
|
|
366
|
+
result.models?.availableModels,
|
|
367
|
+
result._meta?.antigravity?.tier,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
this.host.onModelsChanged()
|
|
371
|
+
} finally {
|
|
372
|
+
try {
|
|
373
|
+
await proc.closeSession(result.sessionId)
|
|
374
|
+
} finally {
|
|
375
|
+
removeSessionArtifacts(this.home, result.sessionId)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
})().finally(() => {
|
|
379
|
+
this.warmupPromise = undefined
|
|
380
|
+
})
|
|
381
|
+
return this.warmupPromise
|
|
365
382
|
}
|
|
366
383
|
|
|
367
384
|
/** Stop every session, the process, and the bridge. */
|
|
@@ -593,7 +610,10 @@ export class AntigravityRuntime {
|
|
|
593
610
|
}
|
|
594
611
|
|
|
595
612
|
private runPreflight(): Promise<PreflightResult> {
|
|
596
|
-
if (this.preflight !== undefined)
|
|
613
|
+
if (this.preflight !== undefined) {
|
|
614
|
+
prepareHome(this.home)
|
|
615
|
+
return Promise.resolve(this.preflight)
|
|
616
|
+
}
|
|
597
617
|
this.preflightPromise ??= (async () => {
|
|
598
618
|
const config = this.host.config
|
|
599
619
|
const binary = resolveAgyBinary(config.command)
|
|
@@ -966,7 +986,10 @@ export class AntigravityRuntime {
|
|
|
966
986
|
|
|
967
987
|
private async resetSession(conv: Conversation): Promise<void> {
|
|
968
988
|
if (conv.sessionId !== undefined && conv.process !== undefined) {
|
|
969
|
-
|
|
989
|
+
const sessionId = conv.sessionId
|
|
990
|
+
void conv.process
|
|
991
|
+
.closeSession(sessionId)
|
|
992
|
+
.finally(() => removeSessionArtifacts(this.home, sessionId))
|
|
970
993
|
}
|
|
971
994
|
this.releaseTenant(conv)
|
|
972
995
|
conv.sessionId = undefined
|