neuralos 2.9.9 → 2.9.11
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/bin/gybackend.cjs +11 -1
- package/package.json +2 -5
- package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +3 -3
- package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +282 -0
- package/plugins/agentspan-bridge/conductorClient.mjs +16 -0
- package/plugins/agentspan-bridge/index.mjs +114 -0
- package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +209 -0
- package/plugins/agentspan-bridge/plugin.json +4 -1
package/bin/gybackend.cjs
CHANGED
|
@@ -347146,13 +347146,23 @@ async function manageRecording(args, context2) {
|
|
|
347146
347146
|
if (!args.terminalId) {
|
|
347147
347147
|
out = "start requires terminalId.";
|
|
347148
347148
|
} else {
|
|
347149
|
-
|
|
347149
|
+
const ts = context2.terminalService;
|
|
347150
|
+
const id = ts && typeof ts.startRecording === "function" ? ts.startRecording(args.terminalId, { title: args.title }) : r.start(args.terminalId, { title: args.title });
|
|
347151
|
+
out = `Recording started: ${id}`;
|
|
347150
347152
|
}
|
|
347151
347153
|
} else if (args.action === "stop") {
|
|
347152
347154
|
if (!args.recordingId) {
|
|
347153
347155
|
out = "stop requires recordingId.";
|
|
347154
347156
|
} else {
|
|
347155
347157
|
const rec = r.stop(args.recordingId);
|
|
347158
|
+
try {
|
|
347159
|
+
const ts = context2.terminalService;
|
|
347160
|
+
const termId = rec.terminalId ?? args.terminalId;
|
|
347161
|
+
if (termId && ts?.activeRecordings instanceof Map) {
|
|
347162
|
+
if (ts.activeRecordings.get(termId) === args.recordingId) ts.activeRecordings.delete(termId);
|
|
347163
|
+
}
|
|
347164
|
+
} catch {
|
|
347165
|
+
}
|
|
347156
347166
|
out = `Recording ${args.recordingId} stopped (${rec.events.length} events).`;
|
|
347157
347167
|
}
|
|
347158
347168
|
} else if (args.action === "replay") {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuralos",
|
|
3
|
-
"version": "2.9.
|
|
4
|
-
"description": "neuralOS — the headless AI-native backend for RTerm. run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan/Conductor durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v2.9.
|
|
3
|
+
"version": "2.9.11",
|
|
4
|
+
"description": "neuralOS — the headless AI-native backend for RTerm. run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan/Conductor durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v2.9.11: fix — agent-tool session recording now captures (start routed via TerminalService).",
|
|
5
5
|
"main": "bin/gybackend.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"neuralos": "bin/gybackend.cjs",
|
|
@@ -63,9 +63,6 @@
|
|
|
63
63
|
"apm",
|
|
64
64
|
"dem",
|
|
65
65
|
"etw",
|
|
66
|
-
"agentspan",
|
|
67
|
-
"conductor",
|
|
68
|
-
"durable-agents",
|
|
69
66
|
"monitoring"
|
|
70
67
|
]
|
|
71
68
|
}
|
|
@@ -203,11 +203,11 @@ function makeCtx(fetchImpl, settings = {}) {
|
|
|
203
203
|
return { tools, triggers, panels, logs, ctx }
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
test('register wires
|
|
206
|
+
test('register wires 9 tools, 1 trigger, 1 panel', () => {
|
|
207
207
|
const { tools, triggers, panels, ctx } = makeCtx(null, { serverUrl: 'http://x:6767' })
|
|
208
208
|
register(ctx)
|
|
209
|
-
assert.equal(tools.size,
|
|
210
|
-
for (const n of ['agentspan_health', 'agentspan_run', 'agentspan_status', 'agentspan_approve', 'agentspan_list', 'agentspan_stop']) assert.ok(tools.has(n), `missing ${n}`)
|
|
209
|
+
assert.equal(tools.size, 9)
|
|
210
|
+
for (const n of ['agentspan_health', 'agentspan_run', 'agentspan_status', 'agentspan_approve', 'agentspan_list', 'agentspan_stop', 'agentspan_export_playbook', 'agentspan_register_playbook', 'agentspan_delegate']) assert.ok(tools.has(n), `missing ${n}`)
|
|
211
211
|
assert.equal(triggers.length, 1)
|
|
212
212
|
assert.equal(panels.length, 1)
|
|
213
213
|
})
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentspan-bridge.phase2.extreme.spec.ts — exhaustive offline tests for the
|
|
3
|
+
* Phase-2 additions: playbookToWorkflowDef mapper (step→task mapping, DAG
|
|
4
|
+
* edges, wait/rollback, retries), the conductorClient registerWorkflowDef /
|
|
5
|
+
* getWorkflowDef methods, and the new tools (agentspan_export_playbook,
|
|
6
|
+
* agentspan_register_playbook, agentspan_delegate) with mocked fetch. No network.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { ConductorClient, DEFAULT_BASE_URL } from './conductorClient.mjs'
|
|
11
|
+
import {
|
|
12
|
+
playbookToWorkflowDef,
|
|
13
|
+
stepToTask,
|
|
14
|
+
rollbackToTask,
|
|
15
|
+
taskRef,
|
|
16
|
+
} from './playbookToWorkflowDef.mjs'
|
|
17
|
+
import { register, findPlaybook, buildDelegateAgentConfig } from './index.mjs'
|
|
18
|
+
|
|
19
|
+
// ─── mock fetch ─────────────────────────────────────────────────────────────
|
|
20
|
+
function mockFetch(respond) {
|
|
21
|
+
const calls = []
|
|
22
|
+
const fn = async (url, init) => {
|
|
23
|
+
calls.push({ url, init })
|
|
24
|
+
const r = typeof respond === 'function' ? respond(url, init) : respond
|
|
25
|
+
return { ok: r.ok !== false && (r.status ?? 200) < 400, status: r.status ?? 200, text: async () => r.text ?? (r.json !== undefined ? JSON.stringify(r.json) : '') }
|
|
26
|
+
}
|
|
27
|
+
fn.calls = calls
|
|
28
|
+
return fn
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const samplePlaybook = {
|
|
32
|
+
id: 'pb-1',
|
|
33
|
+
name: 'nightly backup',
|
|
34
|
+
description: 'backup the core switch',
|
|
35
|
+
steps: [
|
|
36
|
+
{ id: 'st-1', name: 'prep', kind: 'command', command: 'term length 0' },
|
|
37
|
+
{ id: 'st-2', name: 'collect', kind: 'script', scriptId: 'scr-9' },
|
|
38
|
+
{ id: 'st-3', name: 'settle', kind: 'wait', waitSeconds: 5 },
|
|
39
|
+
{ id: 'st-4', name: 'apply', kind: 'command', command: 'apply acl', rollback: { kind: 'command', command: 'no acl' }, dependsOn: ['st-1', 'st-2'] },
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── taskRef ────────────────────────────────────────────────────────────────
|
|
44
|
+
test('taskRef sanitizes to Conductor-safe refs', () => {
|
|
45
|
+
assert.equal(taskRef('st-1'), 'st_1')
|
|
46
|
+
assert.equal(taskRef('apply acl!'), 'apply_acl_')
|
|
47
|
+
assert.equal(taskRef(undefined, 'fallback'), 'fallback')
|
|
48
|
+
assert.equal(taskRef(''), 'step')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ─── stepToTask ─────────────────────────────────────────────────────────────
|
|
52
|
+
test('command step → HTTP run_command task with command + validate', () => {
|
|
53
|
+
const t = stepToTask({ id: 'st-1', kind: 'command', command: 'show run', validate: { expect: 'ok' } }, 0, {})
|
|
54
|
+
assert.equal(t.type, 'HTTP')
|
|
55
|
+
assert.equal(t.taskReferenceName, 'st_1')
|
|
56
|
+
const req = t.inputParameters.http_request
|
|
57
|
+
assert.equal(req.method, 'POST')
|
|
58
|
+
assert.equal(req.body.kind, 'run_command')
|
|
59
|
+
assert.equal(req.body.command, 'show run')
|
|
60
|
+
assert.deepEqual(req.body.validate, { expect: 'ok' })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('script step → SIMPLE script-reference task (scriptId, no inline body)', () => {
|
|
64
|
+
const t = stepToTask({ id: 'st-2', kind: 'script', scriptId: 'scr-9', name: 'collect' }, 1, {})
|
|
65
|
+
assert.equal(t.type, 'SIMPLE')
|
|
66
|
+
assert.equal(t.inputParameters.kind, 'rterm_script')
|
|
67
|
+
assert.equal(t.inputParameters.scriptId, 'scr-9')
|
|
68
|
+
assert.equal(t.inputParameters.name, 'collect')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('wait step → Conductor WAIT task with duration', () => {
|
|
72
|
+
const t = stepToTask({ id: 'st-3', kind: 'wait', waitSeconds: 7 }, 2, {})
|
|
73
|
+
assert.equal(t.type, 'WAIT')
|
|
74
|
+
assert.equal(t.inputParameters.duration, 7)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('onError=continue sets retryCount; stop (default) is 0', () => {
|
|
78
|
+
const cont = stepToTask({ id: 'a', kind: 'command', command: 'x', onError: 'continue' }, 0, { continueRetryCount: 3 })
|
|
79
|
+
assert.equal(cont.retryCount, 3)
|
|
80
|
+
const stop = stepToTask({ id: 'b', kind: 'command', command: 'x' }, 1, {})
|
|
81
|
+
assert.equal(stop.retryCount, 0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ─── rollbackToTask ─────────────────────────────────────────────────────────
|
|
85
|
+
test('rollback command → optional compensating HTTP task', () => {
|
|
86
|
+
const t = rollbackToTask({ kind: 'command', command: 'no acl' }, 'st_4', 0)
|
|
87
|
+
assert.equal(t.type, 'HTTP')
|
|
88
|
+
assert.equal(t.optional, true)
|
|
89
|
+
assert.equal(t.inputParameters.http_request.body.compensating, true)
|
|
90
|
+
assert.match(t.taskReferenceName, /^rollback_st_4_/)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('rollback script → optional compensating SIMPLE task', () => {
|
|
94
|
+
const t = rollbackToTask({ kind: 'script', scriptId: 'undo' }, 'st_1', 1)
|
|
95
|
+
assert.equal(t.type, 'SIMPLE')
|
|
96
|
+
assert.equal(t.inputParameters.scriptId, 'undo')
|
|
97
|
+
assert.equal(t.optional, true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// ─── playbookToWorkflowDef: full mapping ────────────────────────────────────
|
|
101
|
+
test('maps all 4 steps + rollback compensating task in order', () => {
|
|
102
|
+
const def = playbookToWorkflowDef(samplePlaybook, { execUri: 'http://gw:17888/rpc/exec' })
|
|
103
|
+
assert.equal(def.name, 'nightly_backup')
|
|
104
|
+
assert.equal(def.version, 1)
|
|
105
|
+
assert.equal(def.schemaVersion, 2)
|
|
106
|
+
assert.equal(def.restartable, true)
|
|
107
|
+
// 4 step tasks + 1 JOIN (st-4 has 2 deps) + 1 rollback = 6
|
|
108
|
+
const types = def.tasks.map((t) => t.type)
|
|
109
|
+
assert.equal(types.filter((x) => x === 'JOIN').length, 1, 'one JOIN for the 2-dep step')
|
|
110
|
+
assert.equal(types.filter((x) => x === 'WAIT').length, 1, 'one WAIT')
|
|
111
|
+
const rollback = def.tasks[def.tasks.length - 1]
|
|
112
|
+
assert.equal(rollback.optional, true, 'last task is the compensating rollback')
|
|
113
|
+
assert.equal(rollback.inputParameters.http_request.body.command, 'no acl')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('JOIN carries the dependsOn edges (fan-in)', () => {
|
|
117
|
+
const def = playbookToWorkflowDef(samplePlaybook, {})
|
|
118
|
+
const join = def.tasks.find((t) => t.type === 'JOIN')
|
|
119
|
+
assert.deepEqual(join.joinOn, ['st_1', 'st_2'])
|
|
120
|
+
// the dependent task (st-4) appears after the JOIN in order
|
|
121
|
+
const joinIdx = def.tasks.indexOf(join)
|
|
122
|
+
const st4 = def.tasks.find((t) => t.taskReferenceName === 'st_4')
|
|
123
|
+
assert.ok(def.tasks.indexOf(st4) > joinIdx, 'dependent task comes after its JOIN')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('linear playbook (no dependsOn) emits no JOINs', () => {
|
|
127
|
+
const pb = { name: 'linear', steps: [
|
|
128
|
+
{ id: 'a', kind: 'command', command: '1' },
|
|
129
|
+
{ id: 'b', kind: 'command', command: '2' },
|
|
130
|
+
{ id: 'c', kind: 'wait', waitSeconds: 1 },
|
|
131
|
+
] }
|
|
132
|
+
const def = playbookToWorkflowDef(pb, {})
|
|
133
|
+
assert.equal(def.tasks.filter((t) => t.type === 'JOIN').length, 0)
|
|
134
|
+
assert.equal(def.tasks.length, 3)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('multiple rollbacks run in reverse step order (undo newest first)', () => {
|
|
138
|
+
const pb = { name: 'multi', steps: [
|
|
139
|
+
{ id: 'a', kind: 'command', command: 'a1', rollback: { kind: 'command', command: 'undo-a' } },
|
|
140
|
+
{ id: 'b', kind: 'command', command: 'b1', rollback: { kind: 'command', command: 'undo-b' } },
|
|
141
|
+
] }
|
|
142
|
+
const def = playbookToWorkflowDef(pb, {})
|
|
143
|
+
const rbs = def.tasks.filter((t) => t.optional)
|
|
144
|
+
assert.equal(rbs.length, 2)
|
|
145
|
+
assert.equal(rbs[0].inputParameters.http_request.body.command, 'undo-b', 'newest rollback first')
|
|
146
|
+
assert.equal(rbs[1].inputParameters.http_request.body.command, 'undo-a')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('execUri flows into the command tasks + inputTemplate', () => {
|
|
150
|
+
const def = playbookToWorkflowDef(samplePlaybook, { execUri: 'http://gw:9000/exec' })
|
|
151
|
+
const cmdTask = def.tasks.find((t) => t.type === 'HTTP')
|
|
152
|
+
assert.equal(cmdTask.inputParameters.http_request.uri, 'http://gw:9000/exec')
|
|
153
|
+
assert.equal(def.inputTemplate.rtermExecUri, 'http://gw:9000/exec')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('rejects a playbook without steps', () => {
|
|
157
|
+
assert.throws(() => playbookToWorkflowDef({ name: 'x' }), /steps array/)
|
|
158
|
+
assert.throws(() => playbookToWorkflowDef(null), /steps array/)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ─── conductorClient: registerWorkflowDef / getWorkflowDef ─────────────────
|
|
162
|
+
test('registerWorkflowDef POSTs an array to /api/metadata/workflow', async () => {
|
|
163
|
+
const f = mockFetch({ json: {} })
|
|
164
|
+
const c = new ConductorClient({ fetchImpl: f })
|
|
165
|
+
const def = playbookToWorkflowDef(samplePlaybook, {})
|
|
166
|
+
await c.registerWorkflowDef(def)
|
|
167
|
+
const call = f.calls[0]
|
|
168
|
+
assert.equal(call.url, `${DEFAULT_BASE_URL}/api/metadata/workflow`)
|
|
169
|
+
assert.equal(call.init.method, 'POST')
|
|
170
|
+
const body = JSON.parse(call.init.body)
|
|
171
|
+
assert.ok(Array.isArray(body), 'body is an array of defs')
|
|
172
|
+
assert.equal(body[0].name, 'nightly_backup')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('registerWorkflowDef accepts an array + rejects empty', async () => {
|
|
176
|
+
const c = new ConductorClient({ fetchImpl: mockFetch({ json: {} }) })
|
|
177
|
+
await assert.rejects(() => c.registerWorkflowDef(), /WorkflowDef/)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('getWorkflowDef GETs by name (+version)', async () => {
|
|
181
|
+
const f = mockFetch({ json: { name: 'x', version: 2 } })
|
|
182
|
+
const c = new ConductorClient({ fetchImpl: f })
|
|
183
|
+
await c.getWorkflowDef('nightly_backup', 2)
|
|
184
|
+
assert.match(f.calls[0].url, /\/api\/metadata\/workflow\/nightly_backup\?version=2$/)
|
|
185
|
+
await assert.rejects(() => c.getWorkflowDef(), /name/)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ─── index helpers: findPlaybook / buildDelegateAgentConfig ────────────────
|
|
189
|
+
test('findPlaybook resolves from AutomationManager then settings, by id or name', () => {
|
|
190
|
+
const pb = { id: 'pb-1', name: 'nightly' }
|
|
191
|
+
const viaAm = findPlaybook({ automationManager: { getPlaybook: (x) => (x === 'pb-1' ? pb : undefined) } }, 'pb-1')
|
|
192
|
+
assert.equal(viaAm.name, 'nightly')
|
|
193
|
+
const viaSettings = findPlaybook({ settings: { automation: { playbooks: [pb] } } }, 'nightly')
|
|
194
|
+
assert.equal(viaSettings.id, 'pb-1')
|
|
195
|
+
assert.equal(findPlaybook({ settings: { automation: { playbooks: [] } } }, 'ghost'), undefined)
|
|
196
|
+
assert.equal(findPlaybook({}, undefined), undefined)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('buildDelegateAgentConfig builds a valid durable AgentConfig', () => {
|
|
200
|
+
const c = buildDelegateAgentConfig('mybot', 'do the thing', { model: 'anthropic/claude-sonnet-4.6' })
|
|
201
|
+
assert.equal(c.name, 'mybot')
|
|
202
|
+
assert.equal(c.model, 'anthropic/claude-sonnet-4.6')
|
|
203
|
+
assert.equal(c.input, 'do the thing')
|
|
204
|
+
const def = buildDelegateAgentConfig(undefined, 't')
|
|
205
|
+
assert.equal(def.name, 'rterm_delegate')
|
|
206
|
+
assert.equal(def.model, 'openai/gpt-4o')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// ─── new tools (mocked server) ─────────────────────────────────────────────
|
|
210
|
+
function makeCtx(playbooks, fetchImpl) {
|
|
211
|
+
const tools = new Map()
|
|
212
|
+
const triggers = []
|
|
213
|
+
const panels = []
|
|
214
|
+
const ctx = {
|
|
215
|
+
settings: { agentspan: { serverUrl: DEFAULT_BASE_URL }, automation: { playbooks } },
|
|
216
|
+
registerTool: (t) => tools.set(t.name, t),
|
|
217
|
+
registerTrigger: (t) => triggers.push(t),
|
|
218
|
+
registerPanel: (p) => panels.push(p),
|
|
219
|
+
log: () => {},
|
|
220
|
+
}
|
|
221
|
+
return { tools, ctx, fetchImpl }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
test('registers 9 tools now (6 phase-1 + 3 phase-2)', () => {
|
|
225
|
+
const { tools, ctx } = makeCtx([], null)
|
|
226
|
+
register(ctx)
|
|
227
|
+
assert.equal(tools.size, 9)
|
|
228
|
+
for (const n of ['agentspan_export_playbook', 'agentspan_register_playbook', 'agentspan_delegate']) assert.ok(tools.has(n), `missing ${n}`)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('agentspan_export_playbook returns the mapped def without registering', async () => {
|
|
232
|
+
const posted = []
|
|
233
|
+
const realFetch = globalThis.fetch
|
|
234
|
+
globalThis.fetch = async (url, init) => { posted.push(url); return { ok: true, status: 200, text: async () => '{}' } }
|
|
235
|
+
const { tools, ctx } = makeCtx([samplePlaybook], null)
|
|
236
|
+
register(ctx)
|
|
237
|
+
const r = await tools.get('agentspan_export_playbook').handler({ playbook: 'nightly backup' })
|
|
238
|
+
assert.equal(r.name, 'nightly_backup')
|
|
239
|
+
assert.ok(r.taskCount >= 5)
|
|
240
|
+
assert.ok(r.def.tasks.some((t) => t.type === 'WAIT'))
|
|
241
|
+
assert.equal(posted.length, 0, 'export is pure — no HTTP calls')
|
|
242
|
+
const missing = await tools.get('agentspan_export_playbook').handler({ playbook: 'ghost' })
|
|
243
|
+
assert.match(missing.error, /not found/)
|
|
244
|
+
globalThis.fetch = realFetch
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('agentspan_register_playbook registers the def on the server', async () => {
|
|
248
|
+
const calls = []
|
|
249
|
+
const realFetch = globalThis.fetch
|
|
250
|
+
globalThis.fetch = async (url, init) => {
|
|
251
|
+
calls.push({ url, method: init.method })
|
|
252
|
+
return { ok: true, status: 200, text: async () => '{}' }
|
|
253
|
+
}
|
|
254
|
+
const { tools, ctx } = makeCtx([samplePlaybook], null)
|
|
255
|
+
register(ctx)
|
|
256
|
+
const r = await tools.get('agentspan_register_playbook').handler({ playbook: 'nightly backup' })
|
|
257
|
+
assert.equal(r.registered, true)
|
|
258
|
+
assert.equal(r.name, 'nightly_backup')
|
|
259
|
+
assert.equal(r.runWith.args.workflow, 'nightly_backup')
|
|
260
|
+
assert.ok(calls.some((c) => c.url.endsWith('/api/metadata/workflow') && c.method === 'POST'))
|
|
261
|
+
globalThis.fetch = realFetch
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('agentspan_delegate builds an AgentConfig and returns executionId + followUp', async () => {
|
|
265
|
+
const realFetch = globalThis.fetch
|
|
266
|
+
let postedBody
|
|
267
|
+
globalThis.fetch = async (url, init) => {
|
|
268
|
+
if (init.method === 'POST') postedBody = JSON.parse(init.body)
|
|
269
|
+
return { ok: true, status: 200, text: async () => JSON.stringify({ executionId: 'exec-del-1' }) }
|
|
270
|
+
}
|
|
271
|
+
const { tools, ctx } = makeCtx([], null)
|
|
272
|
+
register(ctx)
|
|
273
|
+
const bad = await tools.get('agentspan_delegate').handler({})
|
|
274
|
+
assert.match(bad.error, /prompt/)
|
|
275
|
+
const r = await tools.get('agentspan_delegate').handler({ prompt: 'investigate the disk-full on web-01', model: 'openai/gpt-5.6-sol' })
|
|
276
|
+
assert.equal(r.delegated, true)
|
|
277
|
+
assert.equal(r.executionId, 'exec-del-1')
|
|
278
|
+
assert.match(r.uiUrl, /\/execution\/exec-del-1$/)
|
|
279
|
+
assert.equal(postedBody.model, 'openai/gpt-5.6-sol')
|
|
280
|
+
assert.equal(postedBody.input, 'investigate the disk-full on web-01')
|
|
281
|
+
globalThis.fetch = realFetch
|
|
282
|
+
})
|
|
@@ -161,6 +161,22 @@ export class ConductorClient {
|
|
|
161
161
|
const q = new URLSearchParams({ freeText: query, size: String(size), sort: 'startTime:DESC' })
|
|
162
162
|
return this.request('GET', `/api/workflow/search?${q}`)
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// ── Metadata (WorkflowDef registration) ──────────────────────────────────
|
|
166
|
+
/** Register (create/update) a WorkflowDef on the server (POST /api/metadata/workflow).
|
|
167
|
+
* Accepts a single def or an array (Conductor accepts a JSON array of defs). */
|
|
168
|
+
async registerWorkflowDef(defOrDefs) {
|
|
169
|
+
if (!defOrDefs) throw new Error('registerWorkflowDef needs a WorkflowDef')
|
|
170
|
+
const body = Array.isArray(defOrDefs) ? defOrDefs : [defOrDefs]
|
|
171
|
+
return this.request('POST', '/api/metadata/workflow', body)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Get a registered WorkflowDef by name (+ optional version). */
|
|
175
|
+
async getWorkflowDef(name, version) {
|
|
176
|
+
if (!name) throw new Error('getWorkflowDef needs a name')
|
|
177
|
+
const q = version !== undefined ? `?version=${version}` : ''
|
|
178
|
+
return this.request('GET', `/api/metadata/workflow/${encodeURIComponent(name)}${q}`)
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
export default ConductorClient
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { ConductorClient, DEFAULT_BASE_URL, joinUrl } from './conductorClient.mjs'
|
|
24
|
+
import { playbookToWorkflowDef } from './playbookToWorkflowDef.mjs'
|
|
24
25
|
|
|
25
26
|
// ─── config resolution ──────────────────────────────────────────────────────
|
|
26
27
|
|
|
@@ -122,6 +123,48 @@ async function guarded(fn, log) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// ─── playbook resolution (from RTerm AutomationManager / settings) ─────────
|
|
127
|
+
|
|
128
|
+
/** Find a named RTerm playbook from the plugin context (AutomationManager or
|
|
129
|
+
* settings.automation.playbooks), by id or name. */
|
|
130
|
+
export function findPlaybook(ctx = {}, nameOrId) {
|
|
131
|
+
if (!nameOrId) return undefined
|
|
132
|
+
const want = String(nameOrId)
|
|
133
|
+
// 1) AutomationManager (runtime handle)
|
|
134
|
+
try {
|
|
135
|
+
const am = ctx.automationManager
|
|
136
|
+
if (am) {
|
|
137
|
+
if (typeof am.getPlaybook === 'function') {
|
|
138
|
+
const p = am.getPlaybook(want)
|
|
139
|
+
if (p) return p
|
|
140
|
+
}
|
|
141
|
+
if (typeof am.listPlaybooks === 'function') {
|
|
142
|
+
const found = (am.listPlaybooks() || []).find((x) => x.id === want || x.name === want)
|
|
143
|
+
if (found) return found
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch { /* fall through to settings */ }
|
|
147
|
+
// 2) settings.automation.playbooks
|
|
148
|
+
try {
|
|
149
|
+
const s = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
|
|
150
|
+
const list = s.automation?.playbooks || []
|
|
151
|
+
return list.find((x) => x.id === want || x.name === want)
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Build a default durable AgentConfig for `agentspan_delegate` (a simple
|
|
158
|
+
* tool-using ReAct agent compiled + started on the server). */
|
|
159
|
+
export function buildDelegateAgentConfig(name, prompt, opts = {}) {
|
|
160
|
+
return {
|
|
161
|
+
name: name || 'rterm_delegate',
|
|
162
|
+
model: opts.model || 'openai/gpt-4o',
|
|
163
|
+
instructions: opts.instructions || 'You are a durable RTerm delegate agent. Complete the task, calling tools as needed.',
|
|
164
|
+
input: prompt,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
// ─── plugin entry ───────────────────────────────────────────────────────────
|
|
126
169
|
|
|
127
170
|
export function register(ctx) {
|
|
@@ -238,6 +281,75 @@ export function register(ctx) {
|
|
|
238
281
|
}, log),
|
|
239
282
|
})
|
|
240
283
|
|
|
284
|
+
// Tool: agentspan_export_playbook — map a named RTerm playbook to a Conductor
|
|
285
|
+
// WorkflowDef (returns the def; does NOT register it). Pure/dry-run preview.
|
|
286
|
+
registerTool({
|
|
287
|
+
name: 'agentspan_export_playbook',
|
|
288
|
+
description: 'Map an RTerm playbook (by id or name) to a Conductor WorkflowDef and return it — a dry-run preview of what agentspan_register_playbook would register. command steps → HTTP run_command tasks, script steps → script-reference tasks, wait steps → WAIT tasks; sequential + dependsOn DAG + retries + rollback compensating tasks.',
|
|
289
|
+
params: {
|
|
290
|
+
playbook: { type: 'string', description: 'RTerm playbook id or name' },
|
|
291
|
+
execUri: { type: 'string', description: 'Override the RTerm exec URI tasks call (default from settings)', optional: true },
|
|
292
|
+
},
|
|
293
|
+
handler: async (p) => guarded(async () => {
|
|
294
|
+
const pb = findPlaybook(ctx, p?.playbook)
|
|
295
|
+
if (!pb) return { error: `playbook not found: ${p?.playbook ?? '(none given)'}` }
|
|
296
|
+
const execUri = p?.execUri || `${config.gatewayUrl ?? 'http://localhost:17888'}/rpc/exec`
|
|
297
|
+
const def = playbookToWorkflowDef(pb, { execUri })
|
|
298
|
+
return { name: def.name, taskCount: def.tasks.length, def }
|
|
299
|
+
}, log),
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Tool: agentspan_register_playbook — register an RTerm playbook on the
|
|
303
|
+
// AgentSpan server as a Conductor WorkflowDef (so agentspan_run {workflow:<name>}
|
|
304
|
+
// and AgentSpan SUB_WORKFLOW steps can invoke it).
|
|
305
|
+
registerTool({
|
|
306
|
+
name: 'agentspan_register_playbook',
|
|
307
|
+
description: 'Export an RTerm playbook to a Conductor WorkflowDef AND register it on the AgentSpan server (POST /api/metadata/workflow), so it can be run durably via agentspan_run {workflow:<name>} or invoked by AgentSpan agents as a SUB_WORKFLOW step.',
|
|
308
|
+
params: {
|
|
309
|
+
playbook: { type: 'string', description: 'RTerm playbook id or name' },
|
|
310
|
+
execUri: { type: 'string', description: 'Override the RTerm exec URI tasks call (default from settings)', optional: true },
|
|
311
|
+
},
|
|
312
|
+
handler: async (p) => guarded(async () => {
|
|
313
|
+
const pb = findPlaybook(ctx, p?.playbook)
|
|
314
|
+
if (!pb) return { error: `playbook not found: ${p?.playbook ?? '(none given)'}` }
|
|
315
|
+
const execUri = p?.execUri || `${config.gatewayUrl ?? 'http://localhost:17888'}/rpc/exec`
|
|
316
|
+
const def = playbookToWorkflowDef(pb, { execUri })
|
|
317
|
+
await client.registerWorkflowDef(def)
|
|
318
|
+
return {
|
|
319
|
+
registered: true,
|
|
320
|
+
name: def.name,
|
|
321
|
+
taskCount: def.tasks.length,
|
|
322
|
+
runWith: { tool: 'agentspan_run', args: { workflow: def.name } },
|
|
323
|
+
uiUrl: joinUrl(config.serverUrl, `/workflowDef/${def.name}`),
|
|
324
|
+
}
|
|
325
|
+
}, log),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// Tool: agentspan_delegate — run a prompt/task as a durable AgentSpan agent
|
|
329
|
+
// (AgentConfig), returning an executionId that survives RTerm/host restart.
|
|
330
|
+
registerTool({
|
|
331
|
+
name: 'agentspan_delegate',
|
|
332
|
+
description: 'Delegate a long-running task to AgentSpan: runs the prompt as a durable agent on the Conductor server (compiles + starts an AgentConfig) and returns an executionId that survives restarts. Follow up with agentspan_status / agentspan_approve / agentspan_stop.',
|
|
333
|
+
params: {
|
|
334
|
+
prompt: { type: 'string', description: 'The task/prompt for the durable agent' },
|
|
335
|
+
name: { type: 'string', description: 'Agent name (default rterm_delegate)', optional: true },
|
|
336
|
+
model: { type: 'string', description: 'Model id (default openai/gpt-4o)', optional: true },
|
|
337
|
+
instructions: { type: 'string', description: 'Override system instructions', optional: true },
|
|
338
|
+
},
|
|
339
|
+
handler: async (p) => guarded(async () => {
|
|
340
|
+
if (!p?.prompt) return { error: 'agentspan_delegate needs a prompt' }
|
|
341
|
+
const cfg = buildDelegateAgentConfig(p.name, p.prompt, { model: p.model, instructions: p.instructions })
|
|
342
|
+
const r = await client.runAgent(cfg)
|
|
343
|
+
return {
|
|
344
|
+
delegated: true,
|
|
345
|
+
executionId: r.executionId,
|
|
346
|
+
serverUrl: config.serverUrl,
|
|
347
|
+
uiUrl: joinUrl(config.serverUrl, `/execution/${r.executionId}`),
|
|
348
|
+
followUp: { status: 'agentspan_status', approve: 'agentspan_approve', stop: 'agentspan_stop' },
|
|
349
|
+
}
|
|
350
|
+
}, log),
|
|
351
|
+
})
|
|
352
|
+
|
|
241
353
|
// Trigger: agentspan_execution_failed — fires when a durable execution fails.
|
|
242
354
|
registerTrigger({
|
|
243
355
|
name: 'agentspan_execution_failed',
|
|
@@ -272,4 +384,6 @@ export default {
|
|
|
272
384
|
summarizeStatus,
|
|
273
385
|
toExecutionRows,
|
|
274
386
|
isFailedExecution,
|
|
387
|
+
findPlaybook,
|
|
388
|
+
buildDelegateAgentConfig,
|
|
275
389
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playbookToWorkflowDef.mjs — map an RTerm playbook (PlaybookEntry) to a
|
|
3
|
+
* Netflix-Conductor WorkflowDef so AgentSpan agents can invoke it as a step
|
|
4
|
+
* (SUB_WORKFLOW) or RTerm can register + run it durably on the Conductor server.
|
|
5
|
+
*
|
|
6
|
+
* Pure mapping (no I/O):
|
|
7
|
+
* - command step → a run_command-style HTTP task carrying the command + target
|
|
8
|
+
* - script step → an HTTP task that references the saved script id (the
|
|
9
|
+
* Conductor worker resolves the script body by id)
|
|
10
|
+
* - wait step → a Conductor WAIT task (durationSeconds)
|
|
11
|
+
* - sequential + dependsOn DAG ordering → Conductor task `taskReferenceName`
|
|
12
|
+
* graph with JOIN on multiple dependencies
|
|
13
|
+
* - retries (onError=continue → retry, else fail-fast) → Conductor `retryCount`
|
|
14
|
+
* - rollback steps → compensating tasks appended after the main flow
|
|
15
|
+
*
|
|
16
|
+
* The emitted WorkflowDef is valid for `POST /api/metadata/workflow` on any
|
|
17
|
+
* Conductor OSS / AgentSpan server.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Sanitize a string into a Conductor-safe taskReferenceName ([A-Za-z0-9_]). */
|
|
21
|
+
export function taskRef(id, fallback) {
|
|
22
|
+
const s = String(id ?? fallback ?? 'step').replace(/[^A-Za-z0-9_]/g, '_')
|
|
23
|
+
return s.length > 0 ? s : 'step'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Map a single RTerm playbook step to a Conductor task (no edges yet). */
|
|
27
|
+
export function stepToTask(step, index, opts = {}) {
|
|
28
|
+
const ref = taskRef(step.id ?? `step_${index}`)
|
|
29
|
+
const name = step.name || ref
|
|
30
|
+
const base = {
|
|
31
|
+
name,
|
|
32
|
+
taskReferenceName: ref,
|
|
33
|
+
retryCount: step.onError === 'continue' ? (opts.continueRetryCount ?? 0) : 0,
|
|
34
|
+
startDelay: 0,
|
|
35
|
+
optional: false,
|
|
36
|
+
asyncComplete: false,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (step.kind === 'wait') {
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
type: 'WAIT',
|
|
43
|
+
inputParameters: { duration: step.waitSeconds ?? 0 },
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (step.kind === 'script') {
|
|
48
|
+
// Reference the saved script by id; the Conductor-side worker resolves the
|
|
49
|
+
// script body (kept out of the def so it stays compact + secret-free).
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
type: 'SIMPLE',
|
|
53
|
+
inputParameters: {
|
|
54
|
+
kind: 'rterm_script',
|
|
55
|
+
scriptId: step.scriptId,
|
|
56
|
+
name,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// kind === 'command' (default): an HTTP task that invokes RTerm's command
|
|
62
|
+
// execution surface with the command + target scope.
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
type: 'HTTP',
|
|
66
|
+
inputParameters: {
|
|
67
|
+
http_request: {
|
|
68
|
+
uri: opts.execUri ?? '${workflow.input.rtermExecUri}',
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'content-type': 'application/json' },
|
|
71
|
+
body: {
|
|
72
|
+
kind: 'run_command',
|
|
73
|
+
command: step.command ?? '',
|
|
74
|
+
...(step.validate ? { validate: step.validate } : {}),
|
|
75
|
+
},
|
|
76
|
+
connectionTimeOut: opts.connectionTimeOut ?? 5000,
|
|
77
|
+
readTimeOut: opts.readTimeOut ?? 300000,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Map a rollback action to a compensating Conductor task. */
|
|
84
|
+
export function rollbackToTask(rollback, stepRef, index) {
|
|
85
|
+
const ref = taskRef(`rollback_${stepRef}_${index}`)
|
|
86
|
+
if (rollback.kind === 'script') {
|
|
87
|
+
return {
|
|
88
|
+
name: ref,
|
|
89
|
+
taskReferenceName: ref,
|
|
90
|
+
type: 'SIMPLE',
|
|
91
|
+
inputParameters: { kind: 'rterm_script', scriptId: rollback.scriptId, compensating: true },
|
|
92
|
+
retryCount: 0,
|
|
93
|
+
optional: true, // a failing rollback never blocks the workflow result
|
|
94
|
+
asyncComplete: false,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
name: ref,
|
|
99
|
+
taskReferenceName: ref,
|
|
100
|
+
type: 'HTTP',
|
|
101
|
+
inputParameters: {
|
|
102
|
+
http_request: {
|
|
103
|
+
uri: '${workflow.input.rtermExecUri}',
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
body: { kind: 'run_command', command: rollback.command ?? '', compensating: true },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
retryCount: 0,
|
|
110
|
+
optional: true,
|
|
111
|
+
asyncComplete: false,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the ordered task list with DAG edges from dependsOn.
|
|
117
|
+
* Returns { tasks, order } where order is the topological order of step refs.
|
|
118
|
+
* Steps with no dependsOn depend on the previous step (linear chain). Steps
|
|
119
|
+
* with multiple dependsOn get a JOIN task.
|
|
120
|
+
*/
|
|
121
|
+
export function buildDag(steps) {
|
|
122
|
+
const refs = steps.map((s, i) => taskRef(s.id ?? `step_${i}`))
|
|
123
|
+
const tasks = []
|
|
124
|
+
const joins = []
|
|
125
|
+
steps.forEach((s, i) => {
|
|
126
|
+
const ref = refs[i]
|
|
127
|
+
const deps = Array.isArray(s.dependsOn) && s.dependsOn.length > 0
|
|
128
|
+
? s.dependsOn.map((d) => taskRef(d))
|
|
129
|
+
: (i === 0 ? [] : [refs[i - 1]])
|
|
130
|
+
if (deps.length > 1) {
|
|
131
|
+
const joinRef = taskRef(`join_${ref}`)
|
|
132
|
+
joins.push({ ref: joinRef, type: 'JOIN', joinOn: deps, taskReferenceName: joinRef, name: joinRef })
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
return { refs, joins }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Map a full RTerm playbook to a Conductor WorkflowDef.
|
|
140
|
+
* @param {object} playbook PlaybookEntry
|
|
141
|
+
* @param {object} [opts] { execUri, connectionTimeOut, readTimeOut, continueRetryCount }
|
|
142
|
+
* @returns {object} a Conductor WorkflowDef (POST /api/metadata/workflow)
|
|
143
|
+
*/
|
|
144
|
+
export function playbookToWorkflowDef(playbook, opts = {}) {
|
|
145
|
+
if (!playbook || !Array.isArray(playbook.steps)) {
|
|
146
|
+
throw new Error('playbookToWorkflowDef needs a playbook with a steps array')
|
|
147
|
+
}
|
|
148
|
+
const name = String(playbook.name || playbook.id || 'rterm_playbook').replace(/\s+/g, '_')
|
|
149
|
+
const steps = playbook.steps
|
|
150
|
+
const tasks = []
|
|
151
|
+
const rollbackTasks = []
|
|
152
|
+
|
|
153
|
+
// Main flow: map each step, then wire sequential/DAG ordering by emitting
|
|
154
|
+
// tasks in dependency order (Conductor executes top-down; dependsOn edges are
|
|
155
|
+
// expressed via JOINs for fan-in).
|
|
156
|
+
const refs = steps.map((s, i) => taskRef(s.id ?? `step_${i}`))
|
|
157
|
+
const emittedJoins = new Set()
|
|
158
|
+
|
|
159
|
+
steps.forEach((step, i) => {
|
|
160
|
+
const ref = refs[i]
|
|
161
|
+
const task = stepToTask(step, i, opts)
|
|
162
|
+
// Wire dependency: Conductor's default is sequential task order, but we
|
|
163
|
+
// explicitly model dependsOn fan-in as a JOIN before this task when the
|
|
164
|
+
// step has multiple dependencies.
|
|
165
|
+
const deps = Array.isArray(step.dependsOn) && step.dependsOn.length > 0
|
|
166
|
+
? step.dependsOn.map((d) => taskRef(d))
|
|
167
|
+
: []
|
|
168
|
+
if (deps.length > 1 && !emittedJoins.has(ref)) {
|
|
169
|
+
const joinRef = taskRef(`join_${ref}`)
|
|
170
|
+
tasks.push({
|
|
171
|
+
name: joinRef,
|
|
172
|
+
taskReferenceName: joinRef,
|
|
173
|
+
type: 'JOIN',
|
|
174
|
+
joinOn: deps,
|
|
175
|
+
})
|
|
176
|
+
emittedJoins.add(ref)
|
|
177
|
+
}
|
|
178
|
+
tasks.push(task)
|
|
179
|
+
if (step.rollback) {
|
|
180
|
+
rollbackTasks.push(rollbackToTask(step.rollback, ref, rollbackTasks.length))
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Compensating (rollback) tasks run after the main flow, in reverse step
|
|
185
|
+
// order (undo the most recent change first) — marked optional so they never
|
|
186
|
+
// mask the workflow's real result.
|
|
187
|
+
const orderedRollback = [...rollbackTasks].reverse()
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
description: playbook.description || `RTerm playbook: ${playbook.name ?? name}`,
|
|
192
|
+
version: 1,
|
|
193
|
+
tasks: [...tasks, ...orderedRollback],
|
|
194
|
+
inputParameters: [],
|
|
195
|
+
outputParameters: {},
|
|
196
|
+
schemaVersion: 2,
|
|
197
|
+
restartable: true,
|
|
198
|
+
workflowStatusListenerEnabled: false,
|
|
199
|
+
ownerEmail: 'rterm@hyperspace.ng',
|
|
200
|
+
timeoutPolicy: 'ALERT_ONLY',
|
|
201
|
+
timeoutSeconds: 0,
|
|
202
|
+
variables: {},
|
|
203
|
+
inputTemplate: {
|
|
204
|
+
rtermExecUri: opts.execUri ?? 'http://localhost:17888/rpc/exec',
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default { playbookToWorkflowDef, stepToTask, rollbackToTask, taskRef, buildDag }
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"description": "AgentSpan/Conductor bridge for RTerm — run durable, crash-resilient agents and workflows on an AgentSpan (Conductor) server from RTerm. Adds durable agent execution (resume-from-step on crash), plan-execute determinism, enterprise event triggers, and a live execution feed to RTerm's agent + dashboard. Server URL + optional auth are configured in Settings (agentspan block); secrets via the vault.",
|
|
5
5
|
"entry": "index.mjs",
|
|
6
6
|
"tools": [
|
|
7
|
+
"agentspan_health",
|
|
7
8
|
"agentspan_run",
|
|
8
9
|
"agentspan_status",
|
|
9
10
|
"agentspan_approve",
|
|
10
11
|
"agentspan_list",
|
|
11
12
|
"agentspan_stop",
|
|
12
|
-
"
|
|
13
|
+
"agentspan_export_playbook",
|
|
14
|
+
"agentspan_register_playbook",
|
|
15
|
+
"agentspan_delegate"
|
|
13
16
|
],
|
|
14
17
|
"triggers": [
|
|
15
18
|
"agentspan_execution_failed"
|