rterm-backend 3.8.6 → 3.8.8
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
CHANGED
|
@@ -366235,6 +366235,15 @@ var AgentHelpers = class {
|
|
|
366235
366235
|
setEventPublisher(publisher) {
|
|
366236
366236
|
this.eventPublisher = publisher;
|
|
366237
366237
|
}
|
|
366238
|
+
/**
|
|
366239
|
+
* Current event publisher (or null). Lets a runtime CHAIN onto the
|
|
366240
|
+
* publisher temporarily — e.g. the plugin runAgentTask hook captures
|
|
366241
|
+
* `say` events for one mesh-invoke session while passing everything
|
|
366242
|
+
* through to the gateway's broadcast unchanged.
|
|
366243
|
+
*/
|
|
366244
|
+
getEventPublisher() {
|
|
366245
|
+
return this.eventPublisher;
|
|
366246
|
+
}
|
|
366238
366247
|
/**
|
|
366239
366248
|
* Build a temporary history for the action model to make decisions.
|
|
366240
366249
|
* This includes the last 3 special marker messages and recent execution details.
|
|
@@ -368273,6 +368282,11 @@ var AgentService_v2 = class {
|
|
|
368273
368282
|
setEventPublisher(publisher) {
|
|
368274
368283
|
this.helpers.setEventPublisher(publisher);
|
|
368275
368284
|
}
|
|
368285
|
+
/** Current event publisher (or null) — lets a runtime chain onto it
|
|
368286
|
+
* temporarily (plugin runAgentTask hook) without replacing it. */
|
|
368287
|
+
getEventPublisher() {
|
|
368288
|
+
return this.helpers.getEventPublisher();
|
|
368289
|
+
}
|
|
368276
368290
|
/** Wire the connection manager so `manage_ssh_connection` can mutate saved
|
|
368277
368291
|
* connections + notify the UI. Optional; only needed in full runtimes. */
|
|
368278
368292
|
setConnectionManager(manager) {
|
|
@@ -392163,7 +392177,7 @@ var PluginRegistry = class {
|
|
|
392163
392177
|
}
|
|
392164
392178
|
/** Build the default PluginContext for a record (registers into the record's
|
|
392165
392179
|
* capability lists; exec/readLedger/log are delegated to the injected fns). */
|
|
392166
|
-
static defaultContext(record2, exec, readLedger, log, spawnProcess, getSettings) {
|
|
392180
|
+
static defaultContext(record2, exec, readLedger, log, spawnProcess, getSettings, runAgentTask) {
|
|
392167
392181
|
return {
|
|
392168
392182
|
registerTool: (tool2) => {
|
|
392169
392183
|
record2.tools.push(tool2);
|
|
@@ -392184,7 +392198,8 @@ var PluginRegistry = class {
|
|
|
392184
392198
|
...spawnProcess ? { spawnProcess } : {},
|
|
392185
392199
|
...getSettings ? { getSettings, get settings() {
|
|
392186
392200
|
return getSettings();
|
|
392187
|
-
} } : {}
|
|
392201
|
+
} } : {},
|
|
392202
|
+
...runAgentTask ? { runAgentTask } : {}
|
|
392188
392203
|
};
|
|
392189
392204
|
}
|
|
392190
392205
|
};
|
|
@@ -394723,7 +394738,50 @@ function createObservability(deps) {
|
|
|
394723
394738
|
});
|
|
394724
394739
|
},
|
|
394725
394740
|
// Live settings snapshot for plugins that read config blocks (webIntel, agentspan, …).
|
|
394726
|
-
() => deps.settingsService?.getSettings?.() ?? {}
|
|
394741
|
+
() => deps.settingsService?.getSettings?.() ?? {},
|
|
394742
|
+
// Real agent turns for plugins that serve an inbound `invoke` skill
|
|
394743
|
+
// (reactorpro-bridge): a mesh peer sends a prompt, RTerm runs it as a
|
|
394744
|
+
// genuine agent turn and the answer goes back over the mesh.
|
|
394745
|
+
// Chained onto the agent's event publisher for the duration of the
|
|
394746
|
+
// turn: `say` events for OUR session are captured, everything passes
|
|
394747
|
+
// through to the original (gateway) publisher unchanged, and the
|
|
394748
|
+
// publisher is restored in finally. dispatchTask resolves when the
|
|
394749
|
+
// run completes, so the last captured `say` is the final answer.
|
|
394750
|
+
deps.agentService ? async (prompt, opts) => {
|
|
394751
|
+
const agent = deps.agentService;
|
|
394752
|
+
const sessionId = opts?.sessionId ?? `mesh-invoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
394753
|
+
const timeoutMs = Math.min(Math.max(opts?.timeoutMs ?? 17e4, 5e3), 6e5);
|
|
394754
|
+
const previous = agent.getEventPublisher();
|
|
394755
|
+
const sayParts = [];
|
|
394756
|
+
const chained = (sid, ev) => {
|
|
394757
|
+
if (sid === sessionId && ev?.type === "say" && typeof ev.content === "string" && ev.content) {
|
|
394758
|
+
sayParts.push(ev.content);
|
|
394759
|
+
}
|
|
394760
|
+
if (previous) {
|
|
394761
|
+
try {
|
|
394762
|
+
previous(sid, ev);
|
|
394763
|
+
} catch {
|
|
394764
|
+
}
|
|
394765
|
+
}
|
|
394766
|
+
};
|
|
394767
|
+
agent.setEventPublisher(chained);
|
|
394768
|
+
let timer;
|
|
394769
|
+
try {
|
|
394770
|
+
await Promise.race([
|
|
394771
|
+
deps.gatewayService.dispatchTask(sessionId, prompt, void 0),
|
|
394772
|
+
new Promise((_, reject) => {
|
|
394773
|
+
timer = setTimeout(() => reject(new Error(`agent turn timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
394774
|
+
})
|
|
394775
|
+
]);
|
|
394776
|
+
} finally {
|
|
394777
|
+
if (timer) clearTimeout(timer);
|
|
394778
|
+
agent.setEventPublisher((sid, ev) => {
|
|
394779
|
+
if (previous) previous(sid, ev);
|
|
394780
|
+
});
|
|
394781
|
+
}
|
|
394782
|
+
const answer = sayParts.join("");
|
|
394783
|
+
return { ok: answer.trim() !== "", answer, sessionId };
|
|
394784
|
+
} : void 0
|
|
394727
394785
|
),
|
|
394728
394786
|
onLog: deps.onLog
|
|
394729
394787
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.8",
|
|
4
4
|
"description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"forward-deployed-engineer",
|
|
@@ -185,13 +185,30 @@ export async function discoverPeers(ctx, filter = {}) {
|
|
|
185
185
|
if (isPublishAck(msg.data)) continue
|
|
186
186
|
const env = uj(msg.data)
|
|
187
187
|
if (env?.type !== 'respond' || env?.from === cfg.agentId) continue
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// SHAPE: the gateway's handleDiscoverRequest replies with the BARE
|
|
189
|
+
// manifest as the payload (attachPayload(reply, manifest)), and a
|
|
190
|
+
// registry may reply with {agents:[...]}. manifestsFrom on the gateway
|
|
191
|
+
// side accepts both; so do we. Some peers may still nest under
|
|
192
|
+
// payload.manifest, so accept that legacy shape too.
|
|
193
|
+
const p = env?.payload
|
|
194
|
+
const candidates = Array.isArray(p?.agents) ? p.agents
|
|
195
|
+
: (p?.id || p?.identity) ? [p]
|
|
196
|
+
: (p?.manifest?.id || p?.manifest?.identity) ? [p.manifest]
|
|
197
|
+
: []
|
|
198
|
+
for (const m of candidates) {
|
|
199
|
+
const id = m?.id ?? m?.identity
|
|
200
|
+
if (id && id !== cfg.agentId) replies.push(m)
|
|
201
|
+
}
|
|
190
202
|
} catch { /* ignore malformed */ }
|
|
191
203
|
}
|
|
192
204
|
})()
|
|
193
205
|
loop.catch(() => {})
|
|
194
|
-
|
|
206
|
+
// BUG FIX (found live against the real gateway): the discover publish MUST
|
|
207
|
+
// carry a reply subject. The gateway's handleDiscoverRequest returns
|
|
208
|
+
// immediately when message.Reply == "" ("a discovery query with no reply
|
|
209
|
+
// subject cannot be answered"), so the old bare publish made discovery
|
|
210
|
+
// silently return zero peers even though every peer was listening.
|
|
211
|
+
nc.publish(`${cfg.prefix}.registry.discover`, j(signed), { reply: '_INBOX.discover' })
|
|
195
212
|
await new Promise((r) => setTimeout(r, windowMs))
|
|
196
213
|
try { replySub.unsubscribe() } catch { /* best-effort */ }
|
|
197
214
|
|
|
@@ -389,7 +406,7 @@ export async function register(ctx) {
|
|
|
389
406
|
const nc = await connectMesh(ctx)
|
|
390
407
|
const identity = loadOrCreateIdentity(cfg)
|
|
391
408
|
const effCfg = { ...cfg, fingerprint: identity.fingerprint }
|
|
392
|
-
const serveCtx = { skills: skills ?? defaultServeSkills(effCfg, { startedAt }), identity, getSkills: () => serveCtx.skills }
|
|
409
|
+
const serveCtx = { skills: skills ?? defaultServeSkills(effCfg, { startedAt, runAgentTask: ctx.runAgentTask }), identity, getSkills: () => serveCtx.skills }
|
|
393
410
|
if (responderStop) responderStop()
|
|
394
411
|
responderStop = await startResponder(nc, effCfg, serveCtx, log)
|
|
395
412
|
if (heartbeatStop) heartbeatStop()
|
|
@@ -409,7 +426,7 @@ export async function register(ctx) {
|
|
|
409
426
|
skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output (defaults to ping/describe/status/invoke)', optional: true },
|
|
410
427
|
},
|
|
411
428
|
handler: async (p) => guarded(async () => {
|
|
412
|
-
const base = defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg).fingerprint }, { startedAt })
|
|
429
|
+
const base = defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg).fingerprint }, { startedAt, runAgentTask: ctx.runAgentTask })
|
|
413
430
|
const skills = p?.skills ?? base
|
|
414
431
|
const served = await serveSkills(skills)
|
|
415
432
|
return { serving: true, inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills: served, note: 'RTerm is now a full ReactorPro mesh citizen (responder + heartbeat + registry live)' }
|
|
@@ -470,7 +487,7 @@ export async function register(ctx) {
|
|
|
470
487
|
if (cfg.enabled && cfg.autoServe) {
|
|
471
488
|
setTimeout(() => {
|
|
472
489
|
Promise.race([
|
|
473
|
-
serveSkills(defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg, log).fingerprint }, { startedAt })),
|
|
490
|
+
serveSkills(defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg, log).fingerprint }, { startedAt, runAgentTask: ctx.runAgentTask })),
|
|
474
491
|
new Promise((_, reject) => setTimeout(() => reject(new Error('auto-serve connect timeout (5s)')), 5000)),
|
|
475
492
|
])
|
|
476
493
|
.then((skills) => log(`[reactorpro] auto-started citizen on ${cfg.prefix}.agent.${cfg.agentId}.inbox (skills: ${skills.join(', ')})`))
|
|
@@ -26,8 +26,10 @@ import { join } from 'node:path'
|
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
AUTH_KEYS,
|
|
29
|
+
buildManifest,
|
|
29
30
|
buildRespond,
|
|
30
31
|
checkIdentity,
|
|
32
|
+
defaultServeSkills,
|
|
31
33
|
envelope,
|
|
32
34
|
fingerprintFor,
|
|
33
35
|
mintIdentity,
|
|
@@ -211,6 +213,140 @@ function ok(cond: unknown, label: string, note = ''): void {
|
|
|
211
213
|
ok(threw, 'a tampered identity file is rejected on load')
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
// ─── A2. Manifest shape contract (the gateway's Manifest struct) ─────────────
|
|
217
|
+
// Found live: the plugin sent `identity` where the gateway's Manifest
|
|
218
|
+
// unmarshals `json:"id"`, and bare-string skills where the gateway expects
|
|
219
|
+
// []{id,name,description} objects. manifestMatches drops a manifest whose ID
|
|
220
|
+
// is empty, so the peer was INVISIBLE to gateway discovery. These pin the
|
|
221
|
+
// corrected contract so it cannot regress.
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
const cfg = { agentId: 'rterm/xcheck/manifest', name: 'XCheck', fingerprint: 'sha256:abc123' }
|
|
225
|
+
const m = buildManifest(cfg, ['ping', 'describe', 'status', 'invoke'])
|
|
226
|
+
ok(m.id === 'rterm/xcheck/manifest',
|
|
227
|
+
'manifest uses id (json:"id") — the gateway drops a manifest with empty ID',
|
|
228
|
+
JSON.stringify(Object.keys(m)))
|
|
229
|
+
ok(!('identity' in m), 'manifest must NOT carry a legacy identity key')
|
|
230
|
+
ok(m.name === 'XCheck', 'manifest name is the display name')
|
|
231
|
+
ok(Array.isArray(m.capabilities) && m.capabilities.includes('rterm'),
|
|
232
|
+
'manifest advertises the rterm capability')
|
|
233
|
+
ok(Array.isArray(m.skills) && m.skills.length === 4
|
|
234
|
+
&& m.skills.every((s: { id: string }) => typeof s?.id === 'string'),
|
|
235
|
+
'skills are []{id,...} objects, not bare strings (the gateway Skill struct unmarshals objects)',
|
|
236
|
+
JSON.stringify(m.skills))
|
|
237
|
+
ok(m.fingerprint === 'sha256:abc123', 'manifest carries the fingerprint for TOFU pinning')
|
|
238
|
+
ok(Array.isArray(m.local_agents) && m.local_agents[0]?.id === cfg.agentId,
|
|
239
|
+
'local_agents entries use id too')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A string skill list must be normalized to objects without dropping any.
|
|
243
|
+
{
|
|
244
|
+
const m = buildManifest({ agentId: 'a' }, ['ping', 'status'])
|
|
245
|
+
ok(m.skills.length === 2 && m.skills[0].id === 'ping' && m.skills[1].id === 'status',
|
|
246
|
+
'string skill ids normalize to {id,name} objects in order')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// A discover reply in the GATEWAY's bare-manifest shape must be accepted by
|
|
250
|
+
// the plugin's parser (the old parser only read payload.manifest). The
|
|
251
|
+
// candidates logic lives inline in discoverPeers; this pins the accepted
|
|
252
|
+
// payload shapes by constructing them the way the gateway does.
|
|
253
|
+
{
|
|
254
|
+
const bare = { id: 'gateway/edge-1', name: 'Edge', skills: [{ id: 'ping' }] }
|
|
255
|
+
const agents = { agents: [{ id: 'gateway/edge-2', name: 'Edge2' }] }
|
|
256
|
+
const legacy = { manifest: { id: 'old/peer', name: 'Old' } }
|
|
257
|
+
const shapes = [bare, agents, legacy]
|
|
258
|
+
for (const p of shapes) {
|
|
259
|
+
const candidates = Array.isArray((p as { agents?: unknown[] }).agents)
|
|
260
|
+
? (p as { agents: unknown[] }).agents
|
|
261
|
+
: ((p as { id?: string }).id || (p as { identity?: string }).identity)
|
|
262
|
+
? [p]
|
|
263
|
+
: ((p as { manifest?: { id?: string } }).manifest?.id
|
|
264
|
+
|| (p as { manifest?: { identity?: string } }).manifest?.identity)
|
|
265
|
+
? [(p as { manifest: unknown }).manifest]
|
|
266
|
+
: []
|
|
267
|
+
ok(candidates.length > 0, `discover parser accepts payload shape ${JSON.stringify(Object.keys(p))}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── A3. Inbound invoke skill (v3.8.8) ───────────────────────────────────────
|
|
272
|
+
// Found live: the manifest ADVERTISED invoke but defaultServeSkills had no
|
|
273
|
+
// handler, so a peer dispatching invoke got SKILL_NOT_FOUND while the
|
|
274
|
+
// manifest promised it. These pin the corrected contract.
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
// A3.1 invoke exists in the default skills and routes to runAgentTask.
|
|
278
|
+
const cfg = { agentId: 'rterm/xcheck/invoke', dispatchTimeout: 180000 }
|
|
279
|
+
const skills = defaultServeSkills(cfg, {
|
|
280
|
+
startedAt: Date.now(),
|
|
281
|
+
runAgentTask: async (prompt: string, opts?: { sessionId?: string; timeoutMs?: number }) => {
|
|
282
|
+
ok(prompt === 'Reply with exactly one word: PONG',
|
|
283
|
+
'invoke passes the prompt through to runAgentTask (arguments.prompt)',
|
|
284
|
+
JSON.stringify({ prompt }))
|
|
285
|
+
ok(typeof opts?.timeoutMs === 'number' && opts.timeoutMs === 170000,
|
|
286
|
+
'invoke stays under the mesh dispatch budget (dispatchTimeout - 10s)',
|
|
287
|
+
JSON.stringify(opts))
|
|
288
|
+
return { ok: true, answer: 'PONG', sessionId: 'mesh-invoke-test' }
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
ok(typeof skills.invoke === 'function', 'defaultServeSkills includes an invoke handler')
|
|
292
|
+
|
|
293
|
+
const r1 = await skills.invoke({ arguments: { prompt: 'Reply with exactly one word: PONG' } } as never)
|
|
294
|
+
ok(r1?.output === 'PONG', 'invoke returns the agent answer as output', JSON.stringify(r1))
|
|
295
|
+
ok(r1?.conversation_id === 'mesh-invoke-test', 'invoke returns conversation_id for session persistence')
|
|
296
|
+
|
|
297
|
+
// A3.2 prompt extraction: text/ prompt/ message all work.
|
|
298
|
+
const skills2 = defaultServeSkills({ agentId: 'a' }, {
|
|
299
|
+
runAgentTask: async (prompt: string) => ({ ok: true, answer: `got:${prompt}`, sessionId: 's' }),
|
|
300
|
+
})
|
|
301
|
+
const viaText = await skills2.invoke({ text: 'hello' } as never)
|
|
302
|
+
ok(viaText?.output === 'got:hello', 'invoke extracts input.text')
|
|
303
|
+
const viaPrompt = await skills2.invoke({ prompt: 'hi' } as never)
|
|
304
|
+
ok(viaPrompt?.output === 'got:hi', 'invoke extracts input.prompt')
|
|
305
|
+
const viaString = await skills2.invoke('plain' as never)
|
|
306
|
+
ok(viaString?.output === 'got:plain', 'invoke extracts a bare string input')
|
|
307
|
+
|
|
308
|
+
// A3.3 conversation_id routes the follow-up to the same agent session.
|
|
309
|
+
const seenSessions: Array<string | undefined> = []
|
|
310
|
+
const skills3 = defaultServeSkills({ agentId: 'a' }, {
|
|
311
|
+
runAgentTask: async (_p: string, o?: { sessionId?: string }) => {
|
|
312
|
+
seenSessions.push(o?.sessionId)
|
|
313
|
+
return { ok: true, answer: 'x', sessionId: o?.sessionId ?? 'new' }
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
const first = await skills3.invoke({ text: 'one' } as never)
|
|
317
|
+
// First call: no conversation_id yet -> the hook generates a session.
|
|
318
|
+
ok(seenSessions[0] === undefined, 'first invoke has no session yet (the hook mints one)')
|
|
319
|
+
await skills3.invoke({ text: 'two', conversation_id: first.conversation_id } as never)
|
|
320
|
+
ok(seenSessions[1] === first.conversation_id && first.conversation_id !== undefined,
|
|
321
|
+
'conversation_id reuses the same agent session on the follow-up',
|
|
322
|
+
JSON.stringify({ seenSessions, firstId: first.conversation_id }))
|
|
323
|
+
|
|
324
|
+
// A3.4 no runAgentTask hook -> a CLEAR error, not SKILL_NOT_FOUND.
|
|
325
|
+
const skills4 = defaultServeSkills({ agentId: 'a' }, {})
|
|
326
|
+
const r4 = await skills4.invoke({ text: 'x' } as never)
|
|
327
|
+
ok(r4?.error?.code === 3002 && String(r4?.error?.message).includes('INVOKE_UNAVAILABLE'),
|
|
328
|
+
'invoke without the runAgentTask hook returns INVOKE_UNAVAILABLE (not SKILL_NOT_FOUND)',
|
|
329
|
+
JSON.stringify(r4))
|
|
330
|
+
|
|
331
|
+
// A3.5 empty prompt -> a clear error.
|
|
332
|
+
const skills5 = defaultServeSkills({ agentId: 'a' }, {
|
|
333
|
+
runAgentTask: async () => ({ ok: true, answer: 'should not run', sessionId: 's' }),
|
|
334
|
+
})
|
|
335
|
+
const r5 = await skills5.invoke({} as never)
|
|
336
|
+
ok(r5?.error?.code === 3003 && String(r5?.error?.message).includes('INVOKE_NEEDS_PROMPT'),
|
|
337
|
+
'invoke with no prompt returns INVOKE_NEEDS_PROMPT without running the agent',
|
|
338
|
+
JSON.stringify(r5))
|
|
339
|
+
|
|
340
|
+
// A3.6 a failed agent turn -> a retryable error carrying the reason.
|
|
341
|
+
const skills6 = defaultServeSkills({ agentId: 'a' }, {
|
|
342
|
+
runAgentTask: async () => ({ ok: false, answer: '', error: 'model offline', sessionId: 's' }),
|
|
343
|
+
})
|
|
344
|
+
const r6 = await skills6.invoke({ text: 'x' } as never)
|
|
345
|
+
ok(r6?.error?.code === 5001 && String(r6?.error?.message).includes('model offline'),
|
|
346
|
+
'a failed agent turn surfaces as a retryable error with the reason',
|
|
347
|
+
JSON.stringify(r6))
|
|
348
|
+
}
|
|
349
|
+
|
|
214
350
|
// ─── B. Go cross-check against the REAL gateway ──────────────────────────────
|
|
215
351
|
|
|
216
352
|
const RP_CHECKOUT = process.env.RP_CHECKOUT ?? join(process.env.HOME ?? '', 'work', 'ReactorPro')
|
|
@@ -224,6 +224,39 @@ export function defaultServeSkills(cfg, deps = {}) {
|
|
|
224
224
|
skills: ['ping', 'describe', 'status', 'invoke'],
|
|
225
225
|
uptime_seconds: Math.floor((Date.now() - (deps.startedAt ?? Date.now())) / 1000),
|
|
226
226
|
}),
|
|
227
|
+
// `invoke` routes a prompt into a REAL RTerm agent turn (tools, policy,
|
|
228
|
+
// audit all apply) and returns the final assistant text. Requires the
|
|
229
|
+
// runAgentTask PluginContext hook — present on the daemon and desktop,
|
|
230
|
+
// absent in bare test harnesses. When absent, return a clear error
|
|
231
|
+
// instead of pretending: a peer must know invoke is unavailable here.
|
|
232
|
+
invoke: async (input = {}) => {
|
|
233
|
+
const runner = deps.runAgentTask
|
|
234
|
+
if (typeof runner !== 'function') {
|
|
235
|
+
return { error: { code: 3002, message: 'INVOKE_UNAVAILABLE: this runtime does not expose agent turns (runAgentTask hook absent)', retryable: false } }
|
|
236
|
+
}
|
|
237
|
+
const prompt = input?.arguments?.prompt
|
|
238
|
+
?? input?.prompt
|
|
239
|
+
?? input?.text
|
|
240
|
+
?? input?.message
|
|
241
|
+
?? (typeof input === 'string' ? input : '')
|
|
242
|
+
if (!prompt || !String(prompt).trim()) {
|
|
243
|
+
return { error: { code: 3003, message: 'INVOKE_NEEDS_PROMPT: pass arguments.prompt (or text)', retryable: false } }
|
|
244
|
+
}
|
|
245
|
+
const conversationId = typeof input?.conversation_id === 'string' ? input.conversation_id : undefined
|
|
246
|
+
const result = await runner(String(prompt), {
|
|
247
|
+
sessionId: conversationId,
|
|
248
|
+
// Stay under the mesh dispatch budget (180s default) so the reply
|
|
249
|
+
// reaches the caller before its own timeout fires.
|
|
250
|
+
timeoutMs: Math.max(5_000, (cfg.dispatchTimeout ?? 180_000) - 10_000),
|
|
251
|
+
})
|
|
252
|
+
if (!result?.ok) {
|
|
253
|
+
return { error: { code: 5001, message: result?.error || 'agent turn produced no answer', retryable: true } }
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
output: result.answer,
|
|
257
|
+
conversation_id: result.sessionId,
|
|
258
|
+
}
|
|
259
|
+
},
|
|
227
260
|
...(deps.skills ?? {}),
|
|
228
261
|
}
|
|
229
262
|
}
|
|
@@ -299,31 +332,50 @@ export function startHeartbeat(nc, cfg, identity, intervalMs = 30000, log = () =
|
|
|
299
332
|
|
|
300
333
|
// ─── 5. REGISTRY ─────────────────────────────────────────────────────────────
|
|
301
334
|
|
|
302
|
-
/** The manifest this RTerm advertises.
|
|
335
|
+
/** The manifest this RTerm advertises.
|
|
336
|
+
*
|
|
337
|
+
* SHAPE CONTRACT (mirrors the gateway's Manifest struct, envelope.go:146):
|
|
338
|
+
* id — json:"id" (NOT "identity"; manifestMatches drops a
|
|
339
|
+
* manifest whose ID is empty, so the old `identity` key made
|
|
340
|
+
* this peer invisible to gateway discovery)
|
|
341
|
+
* skills — []{id, name?, description?} objects, NOT bare strings
|
|
342
|
+
* (the gateway's Skill struct unmarshals objects)
|
|
343
|
+
* fingerprint — json:"fingerprint,omitempty" — pins trust-on-first-use
|
|
344
|
+
* The register/discover payloads are the BARE manifest (the gateway's
|
|
345
|
+
* Register/handleDiscoverRequest both attachPayload(manifest) directly), so
|
|
346
|
+
* callers must not nest it under a "manifest" key. */
|
|
303
347
|
export function buildManifest(cfg, skillIds) {
|
|
348
|
+
const skills = (skillIds ?? []).map((s) =>
|
|
349
|
+
typeof s === 'string' ? { id: s, name: s } : s)
|
|
304
350
|
return {
|
|
305
|
-
|
|
351
|
+
id: cfg.agentId,
|
|
306
352
|
name: cfg.name ?? cfg.agentId,
|
|
307
353
|
capabilities: ['rterm', 'agent', ...(cfg.capabilities ?? [])],
|
|
308
|
-
skills
|
|
354
|
+
skills,
|
|
309
355
|
fingerprint: cfg.fingerprint ?? null,
|
|
310
356
|
local_agents: [{ id: cfg.agentId, name: cfg.name ?? cfg.agentId, capabilities: ['rterm', 'agent'] }],
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
359
|
|
|
314
|
-
/** Register with the mesh registry (mesh.registry.register).
|
|
360
|
+
/** Register with the mesh registry (mesh.registry.register).
|
|
361
|
+
* Payload is the BARE manifest — the gateway's Register does
|
|
362
|
+
* attachPayload(envelope, a.Manifest()) with no wrapper object. */
|
|
315
363
|
export function registerManifest(nc, cfg, identity, manifest) {
|
|
316
|
-
const env = envelope('register',
|
|
364
|
+
const env = envelope('register', manifest, cfg, { to: 'REGISTRY' })
|
|
317
365
|
nc.publish(`${cfg.prefix}.registry.register`, j(signEnvelope(env, identity)))
|
|
318
366
|
}
|
|
319
367
|
|
|
320
368
|
/** Graceful exit. */
|
|
321
369
|
export function deregisterManifest(nc, cfg, identity) {
|
|
322
|
-
const env = envelope('register', {
|
|
370
|
+
const env = envelope('register', { id: cfg.agentId, gone: true }, cfg, { to: 'REGISTRY' })
|
|
323
371
|
nc.publish(`${cfg.prefix}.registry.deregister`, j(signEnvelope(env, identity)))
|
|
324
372
|
}
|
|
325
373
|
|
|
326
|
-
/** Answer discovery broadcasts with our manifest.
|
|
374
|
+
/** Answer discovery broadcasts with our manifest.
|
|
375
|
+
* The reply payload is the BARE manifest — the gateway's
|
|
376
|
+
* handleDiscoverRequest does attachPayload(reply, manifest), and its
|
|
377
|
+
* manifestsFrom accepts either {agents:[...]} or a bare manifest with
|
|
378
|
+
* id != "". Nesting under "manifest" would make it invisible. */
|
|
327
379
|
export async function answerDiscovery(nc, cfg, identity, manifest, log = () => {}) {
|
|
328
380
|
const subject = `${cfg.prefix}.registry.discover`
|
|
329
381
|
const sub = nc.subscribe(subject)
|
|
@@ -336,7 +388,7 @@ export async function answerDiscovery(nc, cfg, identity, manifest, log = () => {
|
|
|
336
388
|
const req = uj(msg.data)
|
|
337
389
|
if (req?.from === cfg.agentId) continue // don't answer ourselves
|
|
338
390
|
if (msg.reply) {
|
|
339
|
-
const env = envelope('respond',
|
|
391
|
+
const env = envelope('respond', manifest, cfg, { to: req?.from, in_reply_to: req?.id })
|
|
340
392
|
nc.publish(msg.reply, j(signEnvelope(env, identity)))
|
|
341
393
|
}
|
|
342
394
|
} catch (e) { log(`[reactorpro] discovery answer failed: ${e?.message ?? e}`) }
|