@zooid/core 0.7.0
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/LICENSE +21 -0
- package/dist/index.d.ts +491 -0
- package/dist/index.js +868 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/__fixtures__/ec2-workspace.yaml +16 -0
- package/src/__fixtures__/opencode-vertex-gemini.yaml +34 -0
- package/src/__fixtures__/triage-agent.yaml +40 -0
- package/src/__fixtures__/zooid-dev.yaml +53 -0
- package/src/acp-config.test.ts +162 -0
- package/src/acp-registry.test.ts +217 -0
- package/src/acp-registry.ts +235 -0
- package/src/acp-types.test.ts +55 -0
- package/src/acp-types.ts +48 -0
- package/src/approval-correlator.test.ts +129 -0
- package/src/approval-correlator.ts +143 -0
- package/src/config-file.test.ts +35 -0
- package/src/config.test.ts +1317 -0
- package/src/config.ts +712 -0
- package/src/env-interpolation.ts +90 -0
- package/src/example-yaml.test.ts +35 -0
- package/src/index.ts +56 -0
- package/src/transport-context.test.ts +34 -0
- package/src/transport-context.ts +91 -0
- package/src/types.ts +213 -0
- package/src/zooid-config.test.ts +164 -0
- package/src/zooid-helpers.test.ts +54 -0
- package/src/zooid-yaml-sweep.test.ts +389 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import type {
|
|
4
|
+
ApprovalDecision,
|
|
5
|
+
ApprovalRequest,
|
|
6
|
+
} from '@zooid/acp-client'
|
|
7
|
+
|
|
8
|
+
export interface RegisteredApproval {
|
|
9
|
+
approvalId: string
|
|
10
|
+
agentName: string
|
|
11
|
+
sessionId: string
|
|
12
|
+
toolCallId: string
|
|
13
|
+
toolKind?: string
|
|
14
|
+
toolTitle?: string
|
|
15
|
+
toolInput?: unknown
|
|
16
|
+
options: ApprovalRequest['options']
|
|
17
|
+
decisionPromise: Promise<ApprovalDecision>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RegisterOptions {
|
|
21
|
+
/** Wall-clock timeout in ms. 0 = no timeout. */
|
|
22
|
+
timeoutMs?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PendingEntry extends RegisteredApproval {
|
|
26
|
+
resolve(decision: ApprovalDecision): void
|
|
27
|
+
timer?: ReturnType<typeof setTimeout>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Correlates ACP approval requests (mid-prompt, originating in `AcpClient`)
|
|
32
|
+
* with HTTP/transport-side decisions. The transport listens to:
|
|
33
|
+
*
|
|
34
|
+
* - `'registered'` — fires after `register()` so the transport can emit
|
|
35
|
+
* `approval.request` on the right SSE stream.
|
|
36
|
+
* - `'timeout'` — fires when an entry is auto-cancelled by the timer
|
|
37
|
+
* so the transport can emit `approval.timeout`.
|
|
38
|
+
*/
|
|
39
|
+
export class ApprovalCorrelator extends EventEmitter {
|
|
40
|
+
private readonly pending = new Map<string, PendingEntry>()
|
|
41
|
+
private readonly bySession = new Map<string, Set<string>>()
|
|
42
|
+
|
|
43
|
+
register(
|
|
44
|
+
agentName: string,
|
|
45
|
+
sessionId: string,
|
|
46
|
+
req: ApprovalRequest,
|
|
47
|
+
opts: RegisterOptions = {},
|
|
48
|
+
): RegisteredApproval {
|
|
49
|
+
const approvalId = randomUUID()
|
|
50
|
+
let resolve!: (d: ApprovalDecision) => void
|
|
51
|
+
const decisionPromise = new Promise<ApprovalDecision>((r) => {
|
|
52
|
+
resolve = r
|
|
53
|
+
})
|
|
54
|
+
const entry: PendingEntry = {
|
|
55
|
+
approvalId,
|
|
56
|
+
agentName,
|
|
57
|
+
sessionId,
|
|
58
|
+
toolCallId: req.toolCallId,
|
|
59
|
+
toolKind: req.toolKind,
|
|
60
|
+
toolTitle: req.toolTitle,
|
|
61
|
+
toolInput: req.toolInput,
|
|
62
|
+
options: req.options,
|
|
63
|
+
decisionPromise,
|
|
64
|
+
resolve,
|
|
65
|
+
}
|
|
66
|
+
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
67
|
+
entry.timer = setTimeout(() => {
|
|
68
|
+
if (this.pending.get(approvalId) !== entry) return
|
|
69
|
+
entry.resolve({ decision: 'cancel' })
|
|
70
|
+
this.pending.delete(approvalId)
|
|
71
|
+
this.bySession.get(sessionId)?.delete(approvalId)
|
|
72
|
+
this.emit('timeout', { approvalId, sessionId, agentName })
|
|
73
|
+
}, opts.timeoutMs)
|
|
74
|
+
entry.timer.unref?.()
|
|
75
|
+
}
|
|
76
|
+
this.pending.set(approvalId, entry)
|
|
77
|
+
let set = this.bySession.get(sessionId)
|
|
78
|
+
if (!set) {
|
|
79
|
+
set = new Set()
|
|
80
|
+
this.bySession.set(sessionId, set)
|
|
81
|
+
}
|
|
82
|
+
set.add(approvalId)
|
|
83
|
+
this.emit('registered', this.toPublic(entry))
|
|
84
|
+
return this.toPublic(entry)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resolve(
|
|
88
|
+
sessionId: string,
|
|
89
|
+
approvalId: string,
|
|
90
|
+
decision: ApprovalDecision,
|
|
91
|
+
): boolean {
|
|
92
|
+
const entry = this.pending.get(approvalId)
|
|
93
|
+
if (!entry || entry.sessionId !== sessionId) return false
|
|
94
|
+
if (entry.timer) clearTimeout(entry.timer)
|
|
95
|
+
entry.resolve(decision)
|
|
96
|
+
this.pending.delete(approvalId)
|
|
97
|
+
this.bySession.get(sessionId)?.delete(approvalId)
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
cancelSession(sessionId: string): void {
|
|
102
|
+
const ids = this.bySession.get(sessionId)
|
|
103
|
+
if (!ids) return
|
|
104
|
+
for (const id of [...ids]) {
|
|
105
|
+
const entry = this.pending.get(id)
|
|
106
|
+
if (entry) {
|
|
107
|
+
if (entry.timer) clearTimeout(entry.timer)
|
|
108
|
+
entry.resolve({ decision: 'cancel' })
|
|
109
|
+
this.pending.delete(id)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.bySession.delete(sessionId)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
listPending(sessionId: string): RegisteredApproval[] {
|
|
116
|
+
const ids = this.bySession.get(sessionId)
|
|
117
|
+
if (!ids) return []
|
|
118
|
+
const out: RegisteredApproval[] = []
|
|
119
|
+
for (const id of ids) {
|
|
120
|
+
const entry = this.pending.get(id)
|
|
121
|
+
if (entry) out.push(this.toPublic(entry))
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
size(): number {
|
|
127
|
+
return this.pending.size
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private toPublic(entry: PendingEntry): RegisteredApproval {
|
|
131
|
+
return {
|
|
132
|
+
approvalId: entry.approvalId,
|
|
133
|
+
agentName: entry.agentName,
|
|
134
|
+
sessionId: entry.sessionId,
|
|
135
|
+
toolCallId: entry.toolCallId,
|
|
136
|
+
toolKind: entry.toolKind,
|
|
137
|
+
toolTitle: entry.toolTitle,
|
|
138
|
+
toolInput: entry.toolInput,
|
|
139
|
+
options: entry.options,
|
|
140
|
+
decisionPromise: entry.decisionPromise,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { findConfigFile } from './config.js'
|
|
6
|
+
|
|
7
|
+
describe('findConfigFile — zooid.yaml migration', () => {
|
|
8
|
+
let dir: string
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
dir = mkdtempSync(join(tmpdir(), 'zooid-rename-'))
|
|
11
|
+
})
|
|
12
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }))
|
|
13
|
+
|
|
14
|
+
it('finds zooid.yaml when present', () => {
|
|
15
|
+
writeFileSync(join(dir, 'zooid.yaml'), 'runtime: local\n')
|
|
16
|
+
const found = findConfigFile(dir)
|
|
17
|
+
expect(found?.path).toBe(join(dir, 'zooid.yaml'))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('throws a migration error when only workforce.yaml is present', () => {
|
|
21
|
+
writeFileSync(join(dir, 'workforce.yaml'), 'runtime: local\n')
|
|
22
|
+
expect(() => findConfigFile(dir)).toThrow(/workforce\.yaml.*no longer supported.*zooid\.yaml.*ZOD045/i)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns null when neither file exists', () => {
|
|
26
|
+
expect(findConfigFile(dir)).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('prefers zooid.yaml when both exist (and does not throw)', () => {
|
|
30
|
+
writeFileSync(join(dir, 'zooid.yaml'), 'runtime: local\n')
|
|
31
|
+
writeFileSync(join(dir, 'workforce.yaml'), 'runtime: local\n')
|
|
32
|
+
const found = findConfigFile(dir)
|
|
33
|
+
expect(found?.path).toBe(join(dir, 'zooid.yaml'))
|
|
34
|
+
})
|
|
35
|
+
})
|