@jc_stack/ez-agents 0.1.0-beta.13 → 0.1.0-beta.19
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/.dockerignore +3 -0
- package/.env.example +20 -0
- package/AGENTS.md +12 -3
- package/CHANGELOG.md +63 -0
- package/CONTRIBUTING.md +62 -6
- package/Dockerfile +6 -0
- package/README.md +11 -2
- package/bin/ezenciel-agents-watch.mjs +8 -0
- package/compose.workforce-watch.yaml +33 -0
- package/compose.yaml +8 -1
- package/docker/run.ts +1 -1
- package/docs/architecture/ai-selection.md +15 -0
- package/docs/architecture/authority-boundaries.md +24 -1
- package/docs/architecture/telegram-intake.md +1 -1
- package/docs/docker-runtime.md +35 -0
- package/docs/host-service.md +19 -0
- package/docs/pagerduty.md +42 -0
- package/docs/plugin-catalog.md +28 -10
- package/docs/plugin-contributions.md +9 -0
- package/docs/plugins.md +46 -1
- package/docs/releasing.md +20 -9
- package/docs/repair.md +41 -0
- package/docs/responsive-channels.md +57 -0
- package/docs/scheduling.md +32 -4
- package/docs/selective-monitoring.md +12 -4
- package/docs/setup.md +43 -0
- package/docs/trusted-publishing.md +140 -0
- package/docs/upgrades.md +24 -4
- package/docs/workforce-watch.md +101 -0
- package/package.json +9 -4
- package/scripts/generate-publish-caller.mjs +60 -0
- package/scripts/smoke-busy-reply.ts +58 -0
- package/scripts/trusted-beta.mjs +289 -0
- package/src/agent-guidance.ts +9 -0
- package/src/ai-cli.ts +2 -1
- package/src/ai.ts +26 -8
- package/src/client-defaults.ts +29 -13
- package/src/codex-session.ts +4 -2
- package/src/config.ts +29 -1
- package/src/control-state.ts +26 -7
- package/src/desktop-bridge.ts +11 -2
- package/src/event-sources.ts +2 -1
- package/src/execution-authority.ts +2 -1
- package/src/executor.ts +34 -7
- package/src/failure.ts +32 -0
- package/src/host-executor-client.ts +7 -1
- package/src/host-executor.ts +22 -13
- package/src/identity.ts +8 -3
- package/src/inbox.ts +7 -3
- package/src/index.ts +260 -92
- package/src/install-tools.mjs +2 -2
- package/src/menu.ts +8 -6
- package/src/model-policy.ts +18 -0
- package/src/owner.ts +3 -3
- package/src/pagerduty.ts +109 -0
- package/src/plugins/manager.mjs +115 -8
- package/src/plugins/shared.mjs +76 -0
- package/src/repair-policy.ts +13 -0
- package/src/reply-context.ts +71 -0
- package/src/reply-executor.ts +55 -0
- package/src/reply-mcp.ts +23 -0
- package/src/runs.ts +14 -16
- package/src/schedule-cli.ts +36 -7
- package/src/scheduled-tasks.ts +33 -0
- package/src/scheduler.ts +22 -4
- package/src/setup.ts +3 -2
- package/src/software-status.ts +5 -5
- package/src/task-cli.ts +3 -3
- package/src/task-executor.ts +9 -6
- package/src/tasks.ts +35 -17
- package/src/telegram-source.ts +94 -0
- package/src/updates/artifact.mjs +16 -0
- package/src/updates/binding.mjs +3 -1
- package/src/updates/control.mjs +4 -4
- package/src/updates/runtime.mjs +5 -2
- package/src/workforce-watch-cli.ts +14 -0
- package/src/workforce-watch.ts +155 -0
- package/templates/agent/AGENTS.md +10 -2
- package/templates/agent/TOOLS.md +6 -0
- package/templates/agent-guidance.md +24 -0
- package/templates/chat-guidance.md +23 -0
- package/templates/failure-review.md +9 -0
- package/templates/maintainer-purpose.md +15 -0
- package/templates/updates.md +2 -2
- package/test/agent-guidance.test.ts +125 -0
- package/test/ai-cli.test.ts +7 -6
- package/test/ai.test.ts +81 -1
- package/test/busy-reply-relay.test.ts +41 -0
- package/test/client-defaults.test.ts +37 -5
- package/test/codex-context.test.ts +5 -2
- package/test/codex-session.test.ts +4 -2
- package/test/config.test.ts +29 -0
- package/test/event-sources.test.ts +4 -0
- package/test/executor.test.ts +11 -1
- package/test/failure.test.ts +256 -0
- package/test/group-owner.test.ts +36 -0
- package/test/host-executor.test.ts +54 -7
- package/test/intake-relay.test.ts +145 -4
- package/test/model-policy.test.ts +69 -0
- package/test/pagerduty.test.ts +104 -0
- package/test/plugin-manager.test.mjs +52 -2
- package/test/relay.test.ts +2 -2
- package/test/repair-policy.test.ts +23 -0
- package/test/reply.test.ts +153 -0
- package/test/runs.test.ts +7 -0
- package/test/schedule-cli.test.ts +10 -2
- package/test/scheduled-tasks.test.ts +43 -0
- package/test/shared-services.test.mjs +98 -0
- package/test/software-status.test.ts +5 -5
- package/test/task-native.test.ts +2 -2
- package/test/tasks.test.ts +14 -6
- package/test/telegram-source.test.ts +75 -0
- package/test/trusted-beta.test.mjs +224 -0
- package/test/updates.test.mjs +35 -3
- package/test/workforce-watch.test.ts +180 -0
package/src/host-executor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactFailure } from './failure.js'
|
|
1
2
|
import { RunStore } from './runs.js'
|
|
2
3
|
import { Tasks } from './tasks.js'
|
|
3
4
|
import { requireOwnerExecution } from './execution-authority.js'
|
|
@@ -12,10 +13,10 @@ import { taskWorkspace } from './task-workspace.js'
|
|
|
12
13
|
import { packageVersion } from './version.js'
|
|
13
14
|
import { installedPluginVersions } from './software-status.js'
|
|
14
15
|
|
|
15
|
-
export type HostBinding = { name: string; workspace: string; controlDir: string; binDir: string; toolsHome?: string }
|
|
16
|
+
export type HostBinding = { name: string; workspace: string; controlDir: string; binDir: string; toolsHome?: string; sharedWorkspace?: string }
|
|
16
17
|
export type HostInstallation = { cli: string; agents: HostBinding[] }
|
|
17
18
|
|
|
18
|
-
export const serveHostExecutor = async (installation: HostInstallation, signal: AbortSignal) => {
|
|
19
|
+
export const serveHostExecutor = async (installation: HostInstallation, signal: AbortSignal, launch = startExecutorJob) => {
|
|
19
20
|
resolveExecutor(installation.cli)
|
|
20
21
|
if (new Set(installation.agents.map(a=>a.workspace)).size !== installation.agents.length ||
|
|
21
22
|
new Set(installation.agents.map(a=>a.controlDir)).size !== installation.agents.length)
|
|
@@ -24,9 +25,13 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
24
25
|
const busy = new Set<string>()
|
|
25
26
|
const tasks = new Set<Promise<void>>()
|
|
26
27
|
const locks: string[] = []
|
|
28
|
+
const sharedWorkspaces = new Map<HostBinding, string>()
|
|
29
|
+
const catalog = (agent: HostBinding) => readModels(undefined, undefined, path.join(agent.controlDir, 'cli', 'codex'))
|
|
27
30
|
try {
|
|
28
31
|
for (const agent of installation.agents) {
|
|
29
32
|
if (![agent.workspace,agent.controlDir,agent.binDir].every(path.isAbsolute)) throw new Error('Host bindings require absolute paths')
|
|
33
|
+
if (agent.sharedWorkspace && !path.isAbsolute(agent.sharedWorkspace)) throw new Error('Shared workspace requires an absolute path')
|
|
34
|
+
if (agent.sharedWorkspace) sharedWorkspaces.set(agent, await realpath(agent.sharedWorkspace))
|
|
30
35
|
if (agent.toolsHome) {
|
|
31
36
|
if (!path.isAbsolute(agent.toolsHome)) throw new Error('Plugin registry binding requires an absolute path')
|
|
32
37
|
const config=JSON.parse(await readFile(path.join(agent.toolsHome,'config.json'),'utf8'))
|
|
@@ -39,7 +44,7 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
39
44
|
catch(error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
|
|
40
45
|
await writeFile(lock,JSON.stringify({pid:process.pid}),{mode:0o600,flag:'wx'})
|
|
41
46
|
locks.push(lock)
|
|
42
|
-
await writeFile(path.join(directory,'models.json'),JSON.stringify(await
|
|
47
|
+
await writeFile(path.join(directory,'models.json'),JSON.stringify(await catalog(agent)),{mode:0o600})
|
|
43
48
|
// A host crash is terminal for a claimed job. Never replay an action.
|
|
44
49
|
for (const file of await readdir(directory)) if (file.endsWith('.running.json')) {
|
|
45
50
|
const base=path.join(directory,file.slice(0,-13))
|
|
@@ -58,8 +63,8 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
58
63
|
const parent=Number(process.env.EZ_HOST_SUPERVISOR_PID)
|
|
59
64
|
if(parent) { try { process.kill(parent,0) } catch { break } }
|
|
60
65
|
if(Date.now()-catalogAt>30000){
|
|
61
|
-
const models=JSON.stringify(await readModels())
|
|
62
66
|
for(const agent of installation.agents){
|
|
67
|
+
const models=JSON.stringify(await catalog(agent))
|
|
63
68
|
const file=path.join(agent.controlDir,'host-executor/models.json')
|
|
64
69
|
await writeFile(file+'.tmp',models,{mode:0o600});await rename(file+'.tmp',file)
|
|
65
70
|
}
|
|
@@ -74,14 +79,15 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
74
79
|
const id=file.slice(0,-13)
|
|
75
80
|
let run
|
|
76
81
|
try {
|
|
77
|
-
run =
|
|
82
|
+
run = await new RunStore(agent.controlDir).get(id)
|
|
78
83
|
if(id.startsWith('r_schedule_') && !run?.scheduled) throw new Error('Missing scheduled run')
|
|
79
84
|
} catch {
|
|
80
85
|
await appendFile(path.join(directory,id+'.events'),JSON.stringify({stream:'exit',code:1})+'\n',{mode:0o600})
|
|
81
86
|
await rm(path.join(directory,file))
|
|
82
87
|
continue
|
|
83
88
|
}
|
|
84
|
-
const
|
|
89
|
+
const sharedWorkspace=sharedWorkspaces.get(agent)
|
|
90
|
+
const lane=run?.replyOnly ? 'reply:'+agent.name : sharedWorkspace ? 'workspace:'+sharedWorkspace : run?.scheduled ? agent.name+':'+id : agent.name
|
|
85
91
|
if(busy.has(lane) || (run?.scheduled && [...busy].filter(k=>k.startsWith(agent.name+':')).length>=4)) continue
|
|
86
92
|
const base=path.join(directory,id)
|
|
87
93
|
await rename(base+'.request.json',base+'.running.json')
|
|
@@ -103,11 +109,11 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
103
109
|
const opts=request.options as ExecutorOptions
|
|
104
110
|
const cli = opts.cli || installation.cli
|
|
105
111
|
resolveExecutor(cli)
|
|
106
|
-
if (cli !== installation.cli) await validateSelection({id:'selected',name:'Selected model',cli,model:opts.model,effort:opts.effort},await
|
|
107
|
-
const options:ExecutorOptions={workspace:run?.scheduled ? await taskWorkspace(agent.workspace,id) : agent.workspace,controlDir:agent.controlDir,binDir:agent.binDir,toolsHome:agent.toolsHome,cli,
|
|
108
|
-
runId:path.basename(base),timeoutMs:0,
|
|
109
|
-
sessionId:opts.sessionId,isResume:opts.isResume,eventSource:opts.eventSource,model:opts.model,effort:opts.effort}
|
|
110
|
-
job=await
|
|
112
|
+
if (cli !== installation.cli) await validateSelection({id:'selected',name:'Selected model',cli,model:opts.model,effort:opts.effort},await catalog(agent))
|
|
113
|
+
const options:ExecutorOptions={workspace:run?.scheduled ? await taskWorkspace(agent.workspace,id) : agent.workspace,controlDir:agent.controlDir,binDir:agent.binDir,toolsHome:agent.toolsHome,sharedWorkspace,cli,
|
|
114
|
+
runId:path.basename(base),timeoutMs:0,repairEnabled:opts.repairEnabled,
|
|
115
|
+
sessionId:opts.sessionId,isResume:opts.isResume,eventSource:opts.eventSource,model:opts.model,effort:opts.effort,codexAutoCompactTokens:opts.codexAutoCompactTokens}
|
|
116
|
+
job=await launch(request.texts,options)
|
|
111
117
|
active.set(lane,job.child)
|
|
112
118
|
await writeFile(base+'.process.json',JSON.stringify({pid:job.child.pid}),{mode:0o600})
|
|
113
119
|
if(signal.aborted)terminateJob(job.child)
|
|
@@ -115,11 +121,14 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
115
121
|
job.child.stderr?.on('data',chunk=>emit({stream:'stderr',text:chunk.toString()}))
|
|
116
122
|
cancellation=setInterval(()=>{void readFile(base+'.cancel').then(()=>terminateJob(job!.child)).catch(()=>{})},250)
|
|
117
123
|
const code=await new Promise<number>(resolve=>job!.child.once('close',code=>resolve(code??1)))
|
|
124
|
+
if (cancellation) clearInterval(cancellation)
|
|
125
|
+
await job.cleanup()
|
|
126
|
+
job = undefined
|
|
118
127
|
emit({stream:'exit',code})
|
|
119
|
-
} catch { emit({stream:'stderr',text:'Host CLI execution failed\n'}); emit({stream:'exit',code:1}) }
|
|
128
|
+
} catch (error) { emit({stream:'stderr',text:'Host CLI execution failed: '+redactFailure(error instanceof Error ? error.message : 'Unknown error')+'\n'}); emit({stream:'exit',code:1}) }
|
|
120
129
|
finally {
|
|
121
130
|
if(cancellation)clearInterval(cancellation)
|
|
122
|
-
await job?.cleanup()
|
|
131
|
+
await job?.cleanup().catch(() => {})
|
|
123
132
|
await writes
|
|
124
133
|
await rm(base+'.running.json',{force:true})
|
|
125
134
|
await rm(base+'.process.json',{force:true})
|
package/src/identity.ts
CHANGED
|
@@ -6,11 +6,16 @@ export const isOwner = (ctx: Pick<Context, 'from' | 'chat'>, owner: Owner | null
|
|
|
6
6
|
owner &&
|
|
7
7
|
ctx.from &&
|
|
8
8
|
!ctx.from.is_bot &&
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
(owner.kind === 'group'
|
|
10
|
+
? ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup'
|
|
11
|
+
: ctx.chat?.type === 'private' && ctx.from.id === owner.telegramUserId) &&
|
|
12
|
+
ctx.chat?.id === owner.telegramChatId,
|
|
12
13
|
)
|
|
13
14
|
|
|
15
|
+
export const ownsRun = (owner: Owner | null, run: {telegramUserId: number; chatId: number}): boolean =>
|
|
16
|
+
Boolean(owner && Number.isSafeInteger(run.telegramUserId) && run.telegramUserId > 0 &&
|
|
17
|
+
run.chatId === owner.telegramChatId && (owner.kind === 'group' || run.telegramUserId === owner.telegramUserId))
|
|
18
|
+
|
|
14
19
|
export const assertId = (id: string): string => {
|
|
15
20
|
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error('Invalid record identifier')
|
|
16
21
|
return id
|
package/src/inbox.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Update } from 'grammy/types'
|
|
4
4
|
import { isExecutionChoice, type ExecutionChoice } from './ai.js'
|
|
5
|
+
import { isOwner } from './identity.js'
|
|
6
|
+
import type { Owner } from './control-state.js'
|
|
5
7
|
|
|
6
8
|
export type IncomingItem = {
|
|
7
9
|
text: string
|
|
@@ -100,11 +102,13 @@ export class InboxStore {
|
|
|
100
102
|
this.now() - first.receivedAt < 30000
|
|
101
103
|
)
|
|
102
104
|
return
|
|
103
|
-
const
|
|
105
|
+
const chatId = (update: Update) => update.message?.chat.id ?? update.callback_query?.message?.chat.id
|
|
106
|
+
const boundary = state.waiting.findIndex((e) => JSON.stringify(e.execution) !== JSON.stringify(first.execution) || chatId(e.update) !== chatId(first.update))
|
|
104
107
|
const entries = state.waiting.splice(0, boundary < 0 ? 10 : Math.min(10, boundary))
|
|
105
108
|
// Telegram albums contain at most ten items. Don't split one at the batch boundary.
|
|
106
109
|
const album = entries.at(-1)?.update.message?.media_group_id
|
|
107
110
|
while (album && state.waiting[0]?.update.message?.media_group_id === album &&
|
|
111
|
+
chatId(state.waiting[0].update) === chatId(first.update) &&
|
|
108
112
|
JSON.stringify(state.waiting[0].execution) === JSON.stringify(first.execution))
|
|
109
113
|
entries.push(state.waiting.shift()!)
|
|
110
114
|
const batch: InboxBatch = { id: `tg_${first.update.update_id}`, entries, status: 'pending' }
|
|
@@ -135,7 +139,7 @@ export class InboxStore {
|
|
|
135
139
|
})
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
retryLatest(userId: number, chatId: number): Promise<string | undefined> {
|
|
142
|
+
retryLatest(userId: number, chatId: number, owner?: Owner | null): Promise<string | undefined> {
|
|
139
143
|
return this.change((state) => {
|
|
140
144
|
const batch = [...state.batches].reverse().find(
|
|
141
145
|
(b) =>
|
|
@@ -144,7 +148,7 @@ export class InboxStore {
|
|
|
144
148
|
const message = e.update.message || e.update.callback_query?.message
|
|
145
149
|
const from = e.update.message?.from || e.update.callback_query?.from
|
|
146
150
|
return (
|
|
147
|
-
from?.id === userId &&
|
|
151
|
+
owner?.kind === 'group' ? !e.update.message?.sender_chat && isOwner({from, chat: message?.chat}, owner) : from?.id === userId &&
|
|
148
152
|
!from.is_bot &&
|
|
149
153
|
message?.chat.type === 'private' &&
|
|
150
154
|
message.chat.id === chatId
|