@11agents/cli 0.1.2 → 0.1.3
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/README.md +9 -0
- package/bin/11agents.js +19 -1
- package/examples/voc-mcp-tool-call-batch.json +1 -0
- package/examples/x-agent-batch.json +1 -0
- package/examples/x-observation-job-result.json +1 -0
- package/package.json +1 -1
- package/src/commands/knowledge.js +157 -0
- package/src/commands/push.js +1 -0
- package/src/commands/runtime.js +242 -2
- package/src/schema.js +5 -2
package/README.md
CHANGED
|
@@ -64,6 +64,15 @@ Useful daemon options:
|
|
|
64
64
|
11agents daemon start --handler ./worker.js
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Before a claimed task is handed to the built-in Codex worker, the daemon prepares a local read-only project cache:
|
|
68
|
+
|
|
69
|
+
- Cloud database snapshot: `~/.11agents/projects/<project>/database/snapshot.json`
|
|
70
|
+
- Project knowledge base: `~/.11agents/projects/<project>/knowledge_base/`
|
|
71
|
+
- Skill sync state: `~/.11agents/projects/<project>/skills-state.json`
|
|
72
|
+
- Task scratch directory: `~/.11agents/projects/<project>/tmp/<taskId>/`
|
|
73
|
+
|
|
74
|
+
Codex runs from `~/.11agents/projects/<project>/` by default. Treat that directory as read-only project context. Task code may write temporary files only under `./tmp/<taskId>/`; the daemon removes that task scratch directory after the task finishes. Agent environment variables from the control plane are injected into the Codex child process and are not written to disk.
|
|
75
|
+
|
|
67
76
|
The built-in task runner currently supports Codex tasks. A custom handler may export:
|
|
68
77
|
|
|
69
78
|
```js
|
package/bin/11agents.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { parseArgs } from '../src/args.js'
|
|
5
|
+
import { knowledgeStatus, syncKnowledge } from '../src/commands/knowledge.js'
|
|
5
6
|
import { runNode } from '../src/commands/node.js'
|
|
6
7
|
import { pushArtifact, pushBatch, pushObservation } from '../src/commands/push.js'
|
|
7
8
|
import { registerRuntime, scanRuntime, startRuntimeDaemon } from '../src/commands/runtime.js'
|
|
@@ -25,6 +26,8 @@ Usage:
|
|
|
25
26
|
11agents push batch <file>
|
|
26
27
|
11agents push artifact --workspace <slug> --agent <key> --platform x --type post --external-id <id>
|
|
27
28
|
11agents push observation --workspace <slug> --agent <key> --platform x --type post --external-id <id> --metric views=123
|
|
29
|
+
11agents knowledge sync --project <slug> [--mode pull|push]
|
|
30
|
+
11agents knowledge status --project <slug>
|
|
28
31
|
11agents node run --workspace <slug> --agent <key> --node <node-id> --handler ./collect-x.js [--once]
|
|
29
32
|
|
|
30
33
|
Environment:
|
|
@@ -32,7 +35,12 @@ Environment:
|
|
|
32
35
|
GTM_SWARM_SERVER compatibility fallback for server URL
|
|
33
36
|
GTM_WRITES_TOKEN control-plane token for runtime registration
|
|
34
37
|
ELEVENAGENTS_MACHINE stable machine key, defaults to hostname
|
|
35
|
-
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
38
|
+
GTM_SWARM_TOKEN project swarm token for push/node commands
|
|
39
|
+
|
|
40
|
+
Runtime task workspace:
|
|
41
|
+
Built-in Codex tasks run from ~/.11agents/projects/<project>/ by default.
|
|
42
|
+
That directory is read-only project context. Temporary writes belong under
|
|
43
|
+
./tmp/<taskId>/ and are cleaned by the daemon after the task finishes.`)
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
async function main() {
|
|
@@ -123,6 +131,16 @@ async function main() {
|
|
|
123
131
|
return
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
if (command === 'knowledge' && subcommand === 'sync') {
|
|
135
|
+
await syncKnowledge(flags)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === 'knowledge' && subcommand === 'status') {
|
|
140
|
+
await knowledgeStatus(flags)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
126
144
|
if (command === 'node' && subcommand === 'run') {
|
|
127
145
|
await runNode(flags)
|
|
128
146
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { flag } from '../args.js'
|
|
5
|
+
import { requestJson } from '../client.js'
|
|
6
|
+
|
|
7
|
+
function slugify(value) {
|
|
8
|
+
return String(value || 'project')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '') || 'project'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeLocalPath(value, id) {
|
|
16
|
+
const raw = String(value || '').trim()
|
|
17
|
+
if (raw && !raw.startsWith('/') && !raw.split('/').includes('..')) return raw
|
|
18
|
+
return `nodes/${slugify(id)}.md`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeSnapshot(snapshot = {}) {
|
|
22
|
+
return {
|
|
23
|
+
cloud_revision: Number(snapshot.cloud_revision || snapshot.knowledge_base?.cloud_revision || 0),
|
|
24
|
+
local_revision: String(snapshot.local_revision || ''),
|
|
25
|
+
nodes: Array.isArray(snapshot.nodes) ? snapshot.nodes.map((node, index) => {
|
|
26
|
+
const id = slugify(node.id || node.title || `node-${index + 1}`)
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
kind: String(node.kind || 'document'),
|
|
30
|
+
title: String(node.title || id),
|
|
31
|
+
body: String(node.body || ''),
|
|
32
|
+
local_path: safeLocalPath(node.local_path || node.localPath, id),
|
|
33
|
+
metadata: node.metadata && typeof node.metadata === 'object' ? node.metadata : {},
|
|
34
|
+
}
|
|
35
|
+
}).sort((a, b) => a.id.localeCompare(b.id)) : [],
|
|
36
|
+
edges: Array.isArray(snapshot.edges) ? snapshot.edges.map(edge => ({
|
|
37
|
+
from: slugify(edge.from || edge.from_node_id),
|
|
38
|
+
to: slugify(edge.to || edge.to_node_id),
|
|
39
|
+
relation: slugify(edge.relation || 'related'),
|
|
40
|
+
metadata: edge.metadata && typeof edge.metadata === 'object' ? edge.metadata : {},
|
|
41
|
+
})).filter(edge => edge.from && edge.to).sort((a, b) => (
|
|
42
|
+
`${a.from}:${a.to}:${a.relation}`.localeCompare(`${b.from}:${b.to}:${b.relation}`)
|
|
43
|
+
)) : [],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function markdownForNode(node) {
|
|
48
|
+
return [
|
|
49
|
+
`# ${node.title}`,
|
|
50
|
+
'',
|
|
51
|
+
`kind: ${node.kind}`,
|
|
52
|
+
`id: ${node.id}`,
|
|
53
|
+
'',
|
|
54
|
+
node.body || '',
|
|
55
|
+
'',
|
|
56
|
+
].join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function bodyFromMarkdownNode(markdown, fallback = '') {
|
|
60
|
+
const lines = String(markdown || '').split('\n')
|
|
61
|
+
let index = 0
|
|
62
|
+
if (lines[index]?.startsWith('# ')) index += 1
|
|
63
|
+
if (lines[index] === '') index += 1
|
|
64
|
+
if (/^kind:\s*/.test(lines[index] || '')) index += 1
|
|
65
|
+
if (/^id:\s*/.test(lines[index] || '')) index += 1
|
|
66
|
+
if (lines[index] === '') index += 1
|
|
67
|
+
const body = lines.slice(index).join('\n').replace(/\n+$/, '')
|
|
68
|
+
return body || fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildKnowledgeBaseDir(project, { homeDir = os.homedir() } = {}) {
|
|
72
|
+
return path.join(homeDir, '.11agents', 'projects', slugify(project), 'knowledge_base')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function writeKnowledgeSnapshot(project, snapshot, { homeDir = os.homedir() } = {}) {
|
|
76
|
+
const normalized = normalizeSnapshot(snapshot)
|
|
77
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
78
|
+
const localRevision = normalized.local_revision || `${Date.now()}`
|
|
79
|
+
const index = {
|
|
80
|
+
cloud_revision: normalized.cloud_revision,
|
|
81
|
+
local_revision: localRevision,
|
|
82
|
+
nodes: normalized.nodes,
|
|
83
|
+
edges: normalized.edges,
|
|
84
|
+
}
|
|
85
|
+
await mkdir(path.join(baseDir, 'nodes'), { recursive: true })
|
|
86
|
+
await writeFile(path.join(baseDir, 'index.json'), JSON.stringify(index, null, 2))
|
|
87
|
+
await writeFile(path.join(baseDir, 'sync-state.json'), JSON.stringify({
|
|
88
|
+
cloud_revision: normalized.cloud_revision,
|
|
89
|
+
local_revision: localRevision,
|
|
90
|
+
synced_at: new Date().toISOString(),
|
|
91
|
+
}, null, 2))
|
|
92
|
+
for (const node of normalized.nodes) {
|
|
93
|
+
const target = path.join(baseDir, node.local_path)
|
|
94
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
95
|
+
await writeFile(target, markdownForNode(node))
|
|
96
|
+
}
|
|
97
|
+
return index
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function readKnowledgeSnapshot(project, { homeDir = os.homedir() } = {}) {
|
|
101
|
+
const baseDir = buildKnowledgeBaseDir(project, { homeDir })
|
|
102
|
+
const raw = await readFile(path.join(baseDir, 'index.json'), 'utf-8').catch(() => '{}')
|
|
103
|
+
const snapshot = normalizeSnapshot(JSON.parse(raw))
|
|
104
|
+
const nodes = await Promise.all(snapshot.nodes.map(async node => {
|
|
105
|
+
const markdown = await readFile(path.join(baseDir, node.local_path), 'utf-8').catch(() => null)
|
|
106
|
+
if (markdown === null) return node
|
|
107
|
+
return { ...node, body: bodyFromMarkdownNode(markdown, node.body) }
|
|
108
|
+
}))
|
|
109
|
+
return { ...snapshot, nodes }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function syncKnowledge(flags = {}, deps = {}) {
|
|
113
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
114
|
+
if (!project) throw new Error('knowledge sync requires --project <slug>')
|
|
115
|
+
const mode = flag(flags, 'mode', flags.push ? 'push' : 'pull')
|
|
116
|
+
const request = deps.requestJson || requestJson
|
|
117
|
+
const log = deps.log || (value => console.log(value))
|
|
118
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
119
|
+
const localSnapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
120
|
+
const body = mode === 'push'
|
|
121
|
+
? { ...localSnapshot, mode: 'push', actor: 'cli' }
|
|
122
|
+
: { mode: 'pull', local_revision: localSnapshot.local_revision, cloud_revision: localSnapshot.cloud_revision, actor: 'cli' }
|
|
123
|
+
const result = await request(`/api/projects/${project}/knowledge/sync`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body,
|
|
126
|
+
})
|
|
127
|
+
const written = await writeKnowledgeSnapshot(project, result, { homeDir })
|
|
128
|
+
const payload = {
|
|
129
|
+
ok: true,
|
|
130
|
+
project,
|
|
131
|
+
mode,
|
|
132
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
133
|
+
cloud_revision: written.cloud_revision,
|
|
134
|
+
local_revision: written.local_revision,
|
|
135
|
+
nodes: written.nodes.length,
|
|
136
|
+
edges: written.edges.length,
|
|
137
|
+
}
|
|
138
|
+
log(JSON.stringify(payload, null, 2))
|
|
139
|
+
return { ...written, path: payload.path }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function knowledgeStatus(flags = {}, deps = {}) {
|
|
143
|
+
const project = flag(flags, 'project', flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''))
|
|
144
|
+
if (!project) throw new Error('knowledge status requires --project <slug>')
|
|
145
|
+
const homeDir = deps.homeDir || os.homedir()
|
|
146
|
+
const snapshot = await readKnowledgeSnapshot(project, { homeDir })
|
|
147
|
+
const payload = {
|
|
148
|
+
project,
|
|
149
|
+
path: buildKnowledgeBaseDir(project, { homeDir }),
|
|
150
|
+
cloud_revision: snapshot.cloud_revision,
|
|
151
|
+
local_revision: snapshot.local_revision,
|
|
152
|
+
nodes: snapshot.nodes.length,
|
|
153
|
+
edges: snapshot.edges.length,
|
|
154
|
+
}
|
|
155
|
+
;(deps.log || (value => console.log(value)))(JSON.stringify(payload, null, 2))
|
|
156
|
+
return payload
|
|
157
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildTelemetryBatch, validateTelemetryBatch } from '../schema.js'
|
|
|
6
6
|
function defaults(flags) {
|
|
7
7
|
return {
|
|
8
8
|
workspace: flag(flags, 'workspace', process.env.GTM_SWARM_WORKSPACE || ''),
|
|
9
|
+
agent_id: flag(flags, 'agent-id', process.env.GTM_SWARM_AGENT_ID || ''),
|
|
9
10
|
agent_key: flag(flags, 'agent', process.env.GTM_SWARM_AGENT || ''),
|
|
10
11
|
node_id: flag(flags, 'node', process.env.GTM_SWARM_NODE || 'local'),
|
|
11
12
|
}
|
package/src/commands/runtime.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import os from 'node:os'
|
|
3
6
|
import { dirname, resolve } from 'node:path'
|
|
7
|
+
import path from 'node:path'
|
|
4
8
|
import { pathToFileURL } from 'node:url'
|
|
5
9
|
import { fileURLToPath } from 'node:url'
|
|
6
10
|
import { flag } from '../args.js'
|
|
7
11
|
import { getControlConfig, requestJson } from '../client.js'
|
|
12
|
+
import { syncKnowledge } from './knowledge.js'
|
|
8
13
|
import { buildRuntimeScan } from '../runtime-scan.js'
|
|
9
14
|
|
|
10
15
|
const CLI_VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8')).version
|
|
@@ -55,10 +60,12 @@ function runtimeDeps(overrides = {}) {
|
|
|
55
60
|
buildRuntimeScan: overrides.buildRuntimeScan || buildRuntimeScan,
|
|
56
61
|
importHandler: overrides.importHandler || (async handlerPath => import(pathToFileURL(resolve(handlerPath)).href)),
|
|
57
62
|
log: overrides.log || (value => console.log(value)),
|
|
63
|
+
homeDir: overrides.homeDir || os.homedir(),
|
|
58
64
|
runCodex: overrides.runCodex || runCodex,
|
|
59
65
|
runProcess: overrides.runProcess || runProcess,
|
|
60
66
|
requestJson: overrides.requestJson || requestJson,
|
|
61
67
|
sleep: overrides.sleep || sleep,
|
|
68
|
+
syncKnowledge: overrides.syncKnowledge || syncKnowledge,
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -165,10 +172,211 @@ function compactJson(value) {
|
|
|
165
172
|
return JSON.stringify(value ?? null, null, 2)
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
const nonAlphaNum = /[^a-z0-9]+/g
|
|
176
|
+
|
|
177
|
+
function slugify(value, fallback = 'project') {
|
|
178
|
+
return String(value || fallback)
|
|
179
|
+
.trim()
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
.replace(nonAlphaNum, '-')
|
|
182
|
+
.replace(/^-|-$/g, '') || fallback
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sanitizeTaskId(value) {
|
|
186
|
+
return slugify(value, 'task')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function projectSlugForTask(task, flags = {}) {
|
|
190
|
+
return slugify(task.workspace?.slug || task.workspace_slug || flag(flags, 'project') || flag(flags, 'workspace') || 'project')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function projectDirForTask(task, flags = {}, deps) {
|
|
194
|
+
return path.join(deps.homeDir, '.11agents', 'projects', projectSlugForTask(task, flags))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readJsonFile(filePath, fallback = {}) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error?.code === 'ENOENT') return fallback
|
|
202
|
+
return fallback
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableHash(value) {
|
|
207
|
+
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertSafeRelativePath(filePath) {
|
|
211
|
+
const clean = path.normalize(String(filePath || ''))
|
|
212
|
+
if (!clean || path.isAbsolute(clean) || clean === '..' || clean.startsWith(`..${path.sep}`)) {
|
|
213
|
+
throw new Error(`invalid skill file path: ${filePath}`)
|
|
214
|
+
}
|
|
215
|
+
return clean
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function decodeFileContent(file) {
|
|
219
|
+
if (file?.encoding === 'base64') return Buffer.from(String(file.content || ''), 'base64')
|
|
220
|
+
return Buffer.from(String(file?.content || ''), 'utf8')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function skillDirForProvider({ workdir, provider, codexHome }) {
|
|
224
|
+
if (provider === 'codex') return path.join(codexHome || path.join(os.homedir(), '.codex'), 'skills')
|
|
225
|
+
if (provider === 'claude') return path.join(workdir, '.claude', 'skills')
|
|
226
|
+
if (provider === 'copilot') return path.join(workdir, '.github', 'skills')
|
|
227
|
+
if (provider === 'opencode') return path.join(workdir, '.opencode', 'skills')
|
|
228
|
+
if (provider === 'cursor') return path.join(workdir, '.cursor', 'skills')
|
|
229
|
+
return path.join(workdir, '.agent_context', 'skills')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeSkillBundle(skill = {}) {
|
|
233
|
+
return {
|
|
234
|
+
id: String(skill.id || ''),
|
|
235
|
+
name: String(skill.name || 'skill'),
|
|
236
|
+
content: String(skill.content || ''),
|
|
237
|
+
files: Array.isArray(skill.files)
|
|
238
|
+
? skill.files.map(file => ({
|
|
239
|
+
path: String(file.path || ''),
|
|
240
|
+
encoding: file.encoding === 'base64' ? 'base64' : 'utf8',
|
|
241
|
+
content: String(file.content || ''),
|
|
242
|
+
})).sort((a, b) => a.path.localeCompare(b.path))
|
|
243
|
+
: [],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function materializeSkillsIfChanged({ task, workdir, flags, deps }) {
|
|
248
|
+
const skills = Array.isArray(task.agent?.skills) ? task.agent.skills.map(normalizeSkillBundle) : []
|
|
249
|
+
if (!skills.length) return { changed: false, count: 0 }
|
|
250
|
+
|
|
251
|
+
const statePath = path.join(workdir, 'skills-state.json')
|
|
252
|
+
const nextHash = stableHash(skills)
|
|
253
|
+
const current = await readJsonFile(statePath, {})
|
|
254
|
+
if (current.hash === nextHash) return { changed: false, count: skills.length }
|
|
255
|
+
|
|
256
|
+
const provider = task.runtime?.provider || ''
|
|
257
|
+
const skillsDir = skillDirForProvider({
|
|
258
|
+
workdir,
|
|
259
|
+
provider,
|
|
260
|
+
codexHome: flag(flags, 'codex-home', process.env.CODEX_HOME || path.join(deps.homeDir, '.codex')),
|
|
261
|
+
})
|
|
262
|
+
await mkdir(skillsDir, { recursive: true })
|
|
263
|
+
for (const skill of skills) {
|
|
264
|
+
const skillDir = path.join(skillsDir, slugify(skill.name, 'skill'))
|
|
265
|
+
await mkdir(skillDir, { recursive: true })
|
|
266
|
+
await writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf8')
|
|
267
|
+
for (const file of skill.files) {
|
|
268
|
+
const relativePath = assertSafeRelativePath(file.path)
|
|
269
|
+
const target = path.join(skillDir, relativePath)
|
|
270
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
271
|
+
await writeFile(target, decodeFileContent(file))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
await writeFile(statePath, JSON.stringify({
|
|
275
|
+
hash: nextHash,
|
|
276
|
+
count: skills.length,
|
|
277
|
+
skills_dir: skillsDir,
|
|
278
|
+
synced_at: new Date().toISOString(),
|
|
279
|
+
}, null, 2))
|
|
280
|
+
return { changed: true, count: skills.length, skills_dir: skillsDir }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function databaseSyncSpec(task) {
|
|
284
|
+
const spec = task.database || task.cloud_database || task.workspace?.database || null
|
|
285
|
+
if (!spec || typeof spec !== 'object') return null
|
|
286
|
+
const cloudRevision = Number(spec.cloud_revision ?? spec.cloudRevision ?? spec.revision ?? 0)
|
|
287
|
+
const syncRequired = spec.sync_required === true || spec.syncRequired === true
|
|
288
|
+
if (!syncRequired && !cloudRevision) return null
|
|
289
|
+
return { cloudRevision, syncRequired }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function syncDatabaseIfNeeded({ task, workdir, config, deps }) {
|
|
293
|
+
const spec = databaseSyncSpec(task)
|
|
294
|
+
if (!spec) return { synced: false }
|
|
295
|
+
const statePath = path.join(workdir, 'database', 'sync-state.json')
|
|
296
|
+
const current = await readJsonFile(statePath, {})
|
|
297
|
+
const localRevision = Number(current.cloud_revision || 0)
|
|
298
|
+
if (!spec.syncRequired && localRevision === spec.cloudRevision) return { synced: false, cloud_revision: localRevision }
|
|
299
|
+
|
|
300
|
+
const project = projectSlugForTask(task)
|
|
301
|
+
const result = await deps.requestJson(`/api/projects/${project}/database/sync`, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
body: {
|
|
304
|
+
task_id: task.id,
|
|
305
|
+
local_revision: localRevision,
|
|
306
|
+
cloud_revision: spec.cloudRevision,
|
|
307
|
+
},
|
|
308
|
+
config,
|
|
309
|
+
})
|
|
310
|
+
await mkdir(path.dirname(statePath), { recursive: true })
|
|
311
|
+
if (result?.snapshot !== undefined) {
|
|
312
|
+
await writeFile(path.join(path.dirname(statePath), 'snapshot.json'), JSON.stringify(result.snapshot, null, 2))
|
|
313
|
+
}
|
|
314
|
+
await writeFile(statePath, JSON.stringify({
|
|
315
|
+
cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision),
|
|
316
|
+
synced_at: result?.synced_at || new Date().toISOString(),
|
|
317
|
+
}, null, 2))
|
|
318
|
+
return { synced: true, cloud_revision: Number(result?.cloud_revision ?? spec.cloudRevision) }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function agentEnvironment(task) {
|
|
322
|
+
const values = task.agent?.environment_variables || task.agent?.environment || task.agent?.custom_env || {}
|
|
323
|
+
const env = {}
|
|
324
|
+
if (Array.isArray(values)) {
|
|
325
|
+
for (const item of values) {
|
|
326
|
+
const key = String(item?.key || '').trim()
|
|
327
|
+
if (key) env[key] = String(item?.value || '')
|
|
328
|
+
}
|
|
329
|
+
return env
|
|
330
|
+
}
|
|
331
|
+
if (values && typeof values === 'object') {
|
|
332
|
+
for (const [key, value] of Object.entries(values)) {
|
|
333
|
+
const cleanKey = String(key || '').trim()
|
|
334
|
+
if (cleanKey) env[cleanKey] = String(value ?? '')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return env
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function prepareRuntimeTask(task, flags, deps, config) {
|
|
341
|
+
const workdir = flag(flags, 'codex-workdir') || projectDirForTask(task, flags, deps)
|
|
342
|
+
const tmpDir = path.join(workdir, 'tmp', sanitizeTaskId(task.id))
|
|
343
|
+
await mkdir(tmpDir, { recursive: true })
|
|
344
|
+
|
|
345
|
+
const database = await syncDatabaseIfNeeded({ task, workdir, config, deps })
|
|
346
|
+
const skills = await materializeSkillsIfChanged({ task, workdir, flags, deps })
|
|
347
|
+
const env = {
|
|
348
|
+
...process.env,
|
|
349
|
+
...agentEnvironment(task),
|
|
350
|
+
ELEVENAGENTS_PROJECT_DIR: workdir,
|
|
351
|
+
ELEVENAGENTS_TASK_TMP: tmpDir,
|
|
352
|
+
ELEVENAGENTS_TASK_ID: String(task.id || ''),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
workdir,
|
|
357
|
+
tmp_dir: tmpDir,
|
|
358
|
+
project_slug: projectSlugForTask(task, flags),
|
|
359
|
+
readonly: true,
|
|
360
|
+
env,
|
|
361
|
+
database,
|
|
362
|
+
skills,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
168
366
|
function buildCodexPrompt(task) {
|
|
169
367
|
return [
|
|
170
368
|
'You are executing an 11agents task as the assigned agent.',
|
|
171
369
|
'',
|
|
370
|
+
'Execution workspace:',
|
|
371
|
+
compactJson({
|
|
372
|
+
workdir: task.execution_context?.workdir,
|
|
373
|
+
tmp_dir: task.execution_context?.tmp_dir,
|
|
374
|
+
project_slug: task.execution_context?.project_slug,
|
|
375
|
+
knowledge_base: './knowledge_base/',
|
|
376
|
+
rule: 'Treat the project directory as read-only project context except ./knowledge_base/ for durable project knowledge updates and ./tmp/<taskId>/ for temporary scratch files.',
|
|
377
|
+
cleanup: 'Temporary files under ./tmp/<taskId>/ are removed by the CLI after the task finishes.',
|
|
378
|
+
}),
|
|
379
|
+
'',
|
|
172
380
|
'Task context:',
|
|
173
381
|
compactJson({
|
|
174
382
|
queue_event: task.queue_event,
|
|
@@ -176,6 +384,7 @@ function buildCodexPrompt(task) {
|
|
|
176
384
|
issue: task.issue,
|
|
177
385
|
trigger_summary: task.trigger_summary,
|
|
178
386
|
thread_memory: task.thread_memory,
|
|
387
|
+
project_knowledge: task.project_knowledge,
|
|
179
388
|
}),
|
|
180
389
|
'',
|
|
181
390
|
'Assigned agent context:',
|
|
@@ -199,7 +408,7 @@ function buildCodexPrompt(task) {
|
|
|
199
408
|
|
|
200
409
|
async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
201
410
|
const codexBin = flag(flags, 'codex-bin', 'codex')
|
|
202
|
-
const workdir = flag(flags, 'codex-workdir', process.cwd())
|
|
411
|
+
const workdir = flag(flags, 'codex-workdir', task.execution_context?.workdir || process.cwd())
|
|
203
412
|
const args = [
|
|
204
413
|
'--ask-for-approval',
|
|
205
414
|
'never',
|
|
@@ -219,7 +428,7 @@ async function runCodex({ task, prompt, flags = {}, deps }) {
|
|
|
219
428
|
|
|
220
429
|
const commandLine = [codexBin, ...args].map(value => JSON.stringify(String(value))).join(' ')
|
|
221
430
|
deps.log(JSON.stringify({ running: 'codex exec', command: commandLine, workdir }, null, 2))
|
|
222
|
-
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir })
|
|
431
|
+
const result = await deps.runProcess(codexBin, args, { input: prompt, cwd: workdir, env: task.execution_context?.env || process.env })
|
|
223
432
|
const output = String(result.stdout || '').trim()
|
|
224
433
|
const error = String(result.stderr || '').trim()
|
|
225
434
|
if (result.code !== 0) {
|
|
@@ -289,6 +498,21 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
289
498
|
}
|
|
290
499
|
|
|
291
500
|
deps.log(JSON.stringify({ claimed: runtimeTask.id, runtime_id: runtime.id }, null, 2))
|
|
501
|
+
if (runtimeTask.workspace?.slug) {
|
|
502
|
+
await runWithDaemonRetry('sync knowledge base', () => (
|
|
503
|
+
deps.syncKnowledge({
|
|
504
|
+
project: runtimeTask.workspace.slug,
|
|
505
|
+
mode: 'pull',
|
|
506
|
+
server: flags.server,
|
|
507
|
+
token: flags.token,
|
|
508
|
+
}, {
|
|
509
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
510
|
+
log: () => {},
|
|
511
|
+
})
|
|
512
|
+
), deps, retryState)
|
|
513
|
+
}
|
|
514
|
+
const executionContext = await prepareRuntimeTask(runtimeTask, flags, deps, config)
|
|
515
|
+
runtimeTask.execution_context = executionContext
|
|
292
516
|
let completion
|
|
293
517
|
try {
|
|
294
518
|
completion = await handlerModule.handleRuntimeTask(runtimeTask)
|
|
@@ -297,6 +521,22 @@ async function claimAndRunRuntimeTasks(registration, flags, deps, handlerModule,
|
|
|
297
521
|
comment: error instanceof Error ? error.message : String(error),
|
|
298
522
|
status: 'failed',
|
|
299
523
|
}
|
|
524
|
+
} finally {
|
|
525
|
+
await rm(executionContext.tmp_dir, { recursive: true, force: true })
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (runtimeTask.workspace?.slug) {
|
|
529
|
+
await runWithDaemonRetry('sync knowledge base back to cloud', () => (
|
|
530
|
+
deps.syncKnowledge({
|
|
531
|
+
project: runtimeTask.workspace.slug,
|
|
532
|
+
mode: 'push',
|
|
533
|
+
server: flags.server,
|
|
534
|
+
token: flags.token,
|
|
535
|
+
}, {
|
|
536
|
+
requestJson: (apiPath, options = {}) => deps.requestJson(apiPath, { ...options, config }),
|
|
537
|
+
log: () => {},
|
|
538
|
+
})
|
|
539
|
+
), deps, retryState)
|
|
300
540
|
}
|
|
301
541
|
|
|
302
542
|
const body = normalizeTaskCompletion(runtimeTask, completion)
|
package/src/schema.js
CHANGED
|
@@ -19,6 +19,8 @@ function fail(error) {
|
|
|
19
19
|
export function validateTelemetryBatch(input) {
|
|
20
20
|
if (!isObject(input)) return fail('batch must be an object')
|
|
21
21
|
if (input.schema_version !== TELEMETRY_SCHEMA_VERSION) return fail(`schema_version must be ${TELEMETRY_SCHEMA_VERSION}`)
|
|
22
|
+
const agentId = input.agent_id ?? input.agentId
|
|
23
|
+
if (!isNonEmptyString(agentId)) return fail('agent_id is required')
|
|
22
24
|
for (const field of ['workspace', 'agent_key', 'node_id']) {
|
|
23
25
|
if (!isNonEmptyString(input[field])) return fail(`${field} is required`)
|
|
24
26
|
}
|
|
@@ -43,13 +45,14 @@ export function validateTelemetryBatch(input) {
|
|
|
43
45
|
if (typeof value !== 'number' || !Number.isFinite(value)) return fail(`observations[${i}].metrics.${key} must be a finite number`)
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
return { ok: true, batch: input }
|
|
48
|
+
return { ok: true, batch: { ...input, agent_id: agentId.trim() } }
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export function buildTelemetryBatch({ workspace, agent_key, node_id, artifacts = [], observations = [] }) {
|
|
51
|
+
export function buildTelemetryBatch({ workspace, agent_id, agent_key, node_id, artifacts = [], observations = [] }) {
|
|
50
52
|
return {
|
|
51
53
|
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
52
54
|
workspace,
|
|
55
|
+
agent_id,
|
|
53
56
|
agent_key,
|
|
54
57
|
node_id,
|
|
55
58
|
sent_at: new Date().toISOString(),
|