rterm-backend 3.8.7 → 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.7",
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",
@@ -406,7 +406,7 @@ export async function register(ctx) {
406
406
  const nc = await connectMesh(ctx)
407
407
  const identity = loadOrCreateIdentity(cfg)
408
408
  const effCfg = { ...cfg, fingerprint: identity.fingerprint }
409
- 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 }
410
410
  if (responderStop) responderStop()
411
411
  responderStop = await startResponder(nc, effCfg, serveCtx, log)
412
412
  if (heartbeatStop) heartbeatStop()
@@ -426,7 +426,7 @@ export async function register(ctx) {
426
426
  skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output (defaults to ping/describe/status/invoke)', optional: true },
427
427
  },
428
428
  handler: async (p) => guarded(async () => {
429
- const base = defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg).fingerprint }, { startedAt })
429
+ const base = defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg).fingerprint }, { startedAt, runAgentTask: ctx.runAgentTask })
430
430
  const skills = p?.skills ?? base
431
431
  const served = await serveSkills(skills)
432
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)' }
@@ -487,7 +487,7 @@ export async function register(ctx) {
487
487
  if (cfg.enabled && cfg.autoServe) {
488
488
  setTimeout(() => {
489
489
  Promise.race([
490
- serveSkills(defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg, log).fingerprint }, { startedAt })),
490
+ serveSkills(defaultServeSkills({ ...cfg, fingerprint: loadOrCreateIdentity(cfg, log).fingerprint }, { startedAt, runAgentTask: ctx.runAgentTask })),
491
491
  new Promise((_, reject) => setTimeout(() => reject(new Error('auto-serve connect timeout (5s)')), 5000)),
492
492
  ])
493
493
  .then((skills) => log(`[reactorpro] auto-started citizen on ${cfg.prefix}.agent.${cfg.agentId}.inbox (skills: ${skills.join(', ')})`))
@@ -29,6 +29,7 @@ import {
29
29
  buildManifest,
30
30
  buildRespond,
31
31
  checkIdentity,
32
+ defaultServeSkills,
32
33
  envelope,
33
34
  fingerprintFor,
34
35
  mintIdentity,
@@ -267,6 +268,85 @@ function ok(cond: unknown, label: string, note = ''): void {
267
268
  }
268
269
  }
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
+
270
350
  // ─── B. Go cross-check against the REAL gateway ──────────────────────────────
271
351
 
272
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
  }