rterm-backend 3.1.6 → 3.1.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
@@ -367406,6 +367406,10 @@ var AgentService_v2 = class {
367406
367406
  backgroundFileTransferCompleter = null;
367407
367407
  unfinishedBackgroundFileTransferProvider = null;
367408
367408
  imageAttachmentService = null;
367409
+ /** Plugin tools: name → handler (injected from PluginRegistry at boot). */
367410
+ pluginTools = /* @__PURE__ */ new Map();
367411
+ /** Plugin tool schemas (for toolsForModel injection). */
367412
+ pluginToolSchemas = [];
367409
367413
  passChatTempExportService = new PassChatTempExportService();
367410
367414
  fallbackCompactionHistoryExportService = null;
367411
367415
  activeAgentRunIdsBySession = /* @__PURE__ */ new Map();
@@ -367452,6 +367456,18 @@ var AgentService_v2 = class {
367452
367456
  setObservability(obs2) {
367453
367457
  this.observability = obs2 ?? void 0;
367454
367458
  }
367459
+ /** Wire plugin tools (from PluginRegistry) so the agent can call them in chat.
367460
+ * Each plugin tool has: name, description, params (schema), handler (async fn).
367461
+ * The tools are injected into toolsForModel (so the model sees them) and
367462
+ * pluginTools (so the dispatch switch's default case can call them). */
367463
+ setPluginTools(tools2) {
367464
+ this.pluginTools = new Map(tools2.map((t) => [t.name, t.handler]));
367465
+ this.pluginToolSchemas = tools2.map((t) => ({
367466
+ name: t.name,
367467
+ description: t.description,
367468
+ schema: t.params || {}
367469
+ }));
367470
+ }
367455
367471
  /** Wire a session-log handle so list_session_logs / read_session_log work. */
367456
367472
  setSessionLogger(logger) {
367457
367473
  this.sessionLogger = logger ?? void 0;
@@ -367640,6 +367656,7 @@ var AgentService_v2 = class {
367640
367656
  compactionItem?.apiKey ? compactionItem.profile : void 0
367641
367657
  );
367642
367658
  const toolsForModel = buildToolsForModel(readFileSupport);
367659
+ const allToolsForModel = [...toolsForModel, ...this.pluginToolSchemas];
367643
367660
  return {
367644
367661
  profileId,
367645
367662
  model,
@@ -367653,7 +367670,7 @@ var AgentService_v2 = class {
367653
367670
  compactionModelSupportsStructuredOutput,
367654
367671
  compactionModelSupportsObjectToolChoice,
367655
367672
  readFileSupport,
367656
- toolsForModel,
367673
+ toolsForModel: allToolsForModel,
367657
367674
  globalMaxTokens: typeof globalItem.maxTokens === "number" ? globalItem.maxTokens : 2e5,
367658
367675
  thinkingMaxTokens: typeof thinkingItem?.maxTokens === "number" ? thinkingItem.maxTokens : typeof globalItem.maxTokens === "number" ? globalItem.maxTokens : 2e5,
367659
367676
  compactionMaxTokens: typeof compactionItem?.maxTokens === "number" ? compactionItem.maxTokens : typeof thinkingItem?.maxTokens === "number" ? thinkingItem.maxTokens : typeof globalItem.maxTokens === "number" ? globalItem.maxTokens : 2e5
@@ -368952,8 +368969,21 @@ Actually, your intention might be different. Please re-read the description of t
368952
368969
  }
368953
368970
  break;
368954
368971
  }
368955
- default:
368956
- result = `Tool "${toolCall.name}" is not supported.`;
368972
+ default: {
368973
+ const pluginHandler = this.pluginTools.get(toolCall.name);
368974
+ if (pluginHandler) {
368975
+ try {
368976
+ const pluginArgs = typeof toolCall.args === "string" ? JSON.parse(toolCall.args) : toolCall.args || {};
368977
+ const pluginResult = await pluginHandler(pluginArgs);
368978
+ result = typeof pluginResult === "string" ? pluginResult : JSON.stringify(pluginResult);
368979
+ } catch (err) {
368980
+ result = `Plugin tool "${toolCall.name}" error: ${err.message}`;
368981
+ }
368982
+ } else {
368983
+ result = `Tool "${toolCall.name}" is not supported.`;
368984
+ }
368985
+ break;
368986
+ }
368957
368987
  }
368958
368988
  toolMessage.content = result;
368959
368989
  if (shouldInterruptPendingToolsForQueuedInsertion) {
@@ -375199,6 +375229,7 @@ function normalizeSynapseSettings(raw) {
375199
375229
  const hasAuth = auth2 && Object.keys(auth2).length > 0;
375200
375230
  return {
375201
375231
  enabled: src.enabled !== false,
375232
+ autoServe: src.autoServe !== false,
375202
375233
  ...url2 ? { url: url2 } : {},
375203
375234
  ...servers && servers.length > 0 ? { servers } : {},
375204
375235
  ...prefix ? { prefix } : {},
@@ -394721,6 +394752,27 @@ async function startGyBackend() {
394721
394752
  settingsService.onDidChange?.(refreshObservabilityFromSettings);
394722
394753
  agentService.setObservability(observability);
394723
394754
  terminalService.setSessionRecorder(observability.recording);
394755
+ try {
394756
+ const pluginRecords = await observability.pluginRegistry.reload();
394757
+ const pluginTools = [];
394758
+ for (const record2 of pluginRecords) {
394759
+ if (record2.error || !record2.enabled) continue;
394760
+ for (const tool2 of record2.tools) {
394761
+ pluginTools.push({
394762
+ name: tool2.name,
394763
+ description: tool2.description ?? "",
394764
+ params: tool2.params ?? {},
394765
+ handler: tool2.handler
394766
+ });
394767
+ }
394768
+ }
394769
+ if (pluginTools.length > 0) {
394770
+ agentService.setPluginTools(pluginTools);
394771
+ console.log(`[gybackend] Wired ${pluginTools.length} plugin tools from ${pluginRecords.filter((r) => !r.error && r.enabled).length} plugins into the agent.`);
394772
+ }
394773
+ } catch (e) {
394774
+ console.warn("[gybackend] Plugin tool wiring failed:", e instanceof Error ? e.message : String(e));
394775
+ }
394724
394776
  if (settingsService.getSettings().sessionLogging?.enabled) {
394725
394777
  const logDir = import_node_path28.default.join(
394726
394778
  import_node_process2.default.env.GYSHELL_STORE_DIR || "",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rterm-backend",
3
- "version": "3.1.6",
4
- "description": "Headless AI-native backend for RTerm / neuralOS — v3.1.6: full-duplex Synapse agent (responder, emit/subscribe, EXT-REPUTATION scoring, EXT-GOVERNANCE approvals) — RTerm speaks all six Synapse primitives + both extensions. 11 plugins. Transports (SSH/serial/local), SQLite, and NATS libs install automatically.",
3
+ "version": "3.1.8",
4
+ "description": "Headless AI-native backend for RTerm / neuralOS — v3.1.8: plugin tools wired into the agent (all 11 plugins callable in chat). 11 plugins. Transports (SSH/serial/local), SQLite, and NATS libs install automatically.",
5
5
  "main": "bin/gybackend.cjs",
6
6
  "bin": { "gybackend": "bin/gybackend.cjs" },
7
7
  "license": "MIT",
@@ -46,6 +46,8 @@ export function resolveConfig(ctx = {}, env = process.env) {
46
46
  agentId: block.agentId || env.SYNAPSE_AGENT_ID || 'rterm-001',
47
47
  auth: block.auth || undefined,
48
48
  enabled: block.enabled !== false,
49
+ /** auto-start the full-duplex responder on boot (default true when enabled). */
50
+ autoServe: block.autoServe !== false,
49
51
  }
50
52
  }
51
53
 
@@ -264,20 +266,46 @@ export function register(ctx) {
264
266
  // ─── full-duplex agent capabilities (responder, emit/subscribe, reputation, governance) ───
265
267
  const repStore = new ReputationStore()
266
268
  let responderStop = null
269
+ let servingSkills = []
270
+
271
+ /** Start (or restart) the responder with the given skills. Idempotent. */
272
+ async function serveSkills(skills) {
273
+ const nc = await connectMesh(ctx)
274
+ const serveCtx = { ...ctx, rtermSkills: skills ?? ctx.rtermSkills ?? {} }
275
+ if (responderStop) responderStop()
276
+ responderStop = await startResponder(nc, cfg, serveCtx, log)
277
+ servingSkills = Object.keys(serveCtx.rtermSkills)
278
+ return servingSkills
279
+ }
280
+
281
+ /** Default skills RTerm serves when auto-starting (status + discover + dispatch). */
282
+ function defaultServeSkills() {
283
+ return {
284
+ status: async () => ({ up: true, agent: cfg.agentId, ts: new Date().toISOString() }),
285
+ discover: async (inp) => ({ agents: await discoverAgents(ctx, inp ?? {}) }),
286
+ ...(ctx.rtermSkills ?? {}),
287
+ }
288
+ }
267
289
 
268
290
  registerTool({
269
291
  name: 'synapse_serve',
270
- description: 'Start RTerm as a full Synapse agent: listen on mesh.agent.{id}.inbox and respond() to incoming Synapse requests by mapping them to RTerm skills (playbooks/tools via ctx.rtermSkills / getRtermSkills). Bidirectional federation — other agents can now task RTerm.',
292
+ description: 'Start RTerm as a full Synapse agent: listen on mesh.agent.{id}.inbox and respond() to incoming Synapse requests by mapping them to RTerm skills (playbooks/tools via ctx.rtermSkills / getRtermSkills). Bidirectional federation — other agents can now task RTerm. Idempotent; auto-starts on boot when synapse.enabled and autoServe are true.',
271
293
  params: {
272
- skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output, the skills RTerm serves', optional: true },
294
+ skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output, the skills RTerm serves (defaults to status+discover)', optional: true },
273
295
  },
274
296
  handler: async (p) => guarded(async () => {
297
+ const skills = await serveSkills(p?.skills ?? defaultServeSkills())
298
+ return { serving: true, inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills, note: 'RTerm is now a full Synapse agent (responder live)' }
299
+ }, log),
300
+ })
301
+
302
+ registerTool({
303
+ name: 'synapse_serve_status',
304
+ description: 'Report whether the Synapse responder is live (serving on mesh.agent.{id}.inbox) and which skills it serves.',
305
+ params: {},
306
+ handler: async () => guarded(async () => {
275
307
  const nc = await connectMesh(ctx)
276
- const serveCtx = { ...ctx, rtermSkills: p?.skills ?? ctx.rtermSkills ?? {} }
277
- if (responderStop) responderStop() // idempotent: restart with fresh skills
278
- responderStop = await startResponder(nc, cfg, serveCtx, log)
279
- const skillIds = Object.keys(serveCtx.rtermSkills)
280
- return { serving: true, inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills: skillIds, note: 'RTerm is now a full Synapse agent (responder live)' }
308
+ return { serving: responderStop !== null, connected: !nc.isClosed(), inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills: servingSkills, autoServe: cfg.autoServe }
281
309
  }, log),
282
310
  })
283
311
 
@@ -382,7 +410,16 @@ export function register(ctx) {
382
410
  },
383
411
  })
384
412
 
385
- log(`[synapse] synapse-bridge registered: 11 tools, 1 trigger, 1 panel (agent=${cfg.agentId}, prefix=${cfg.prefix}, full-duplex)`)
413
+ log(`[synapse] synapse-bridge registered: 12 tools, 1 trigger, 1 panel (agent=${cfg.agentId}, prefix=${cfg.prefix}, full-duplex)`)
414
+
415
+ // ─── auto-start the full-duplex responder on boot (when enabled + autoServe) ───
416
+ // Makes "be tasked by them" always-on, not opt-in per session. Best-effort: a
417
+ // failed auto-start logs but never blocks plugin registration (server may be down).
418
+ if (cfg.enabled && cfg.autoServe) {
419
+ serveSkills(defaultServeSkills())
420
+ .then((skills) => log(`[synapse] auto-started responder on ${cfg.prefix}.agent.${cfg.agentId}.inbox (skills: ${skills.join(', ')})`))
421
+ .catch((e) => log(`[synapse] auto-serve deferred: ${e?.message ?? e} (responder will start on first synapse_serve call)`))
422
+ }
386
423
  }
387
424
 
388
425
  export default { register, resolveConfig, envelope, discoverAgents, dispatchTask, registerSelf }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "synapse-bridge",
3
3
  "version": "1.0.0",
4
- "description": "Synapse mesh bridge for RTerm — full-duplex Synapse agent: discover live agents, dispatch tasks, register RTerm as a mesh agent, AND serve as one (respond to inbound requests), emit/subscribe mesh events, EXT-REPUTATION scoring, and EXT-GOVERNANCE approvals. Speaks the Synapse protocol (v0.3.0) over a shared NATS server using the v3.1.2 auth/request-reply/JetStream transport. Config in Settings (synapse block); secrets via the vault.",
4
+ "description": "Synapse mesh bridge for RTerm — full-duplex Synapse agent (auto-started on boot): discover live agents, dispatch tasks, register RTerm as a mesh agent, AND serve as one (respond to inbound requests, auto-started), emit/subscribe mesh events, EXT-REPUTATION scoring, and EXT-GOVERNANCE approvals. Speaks the Synapse protocol (v0.3.0) over a shared NATS server using the v3.1.2 auth/request-reply/JetStream transport. Config in Settings (synapse block); secrets via the vault.",
5
5
  "entry": "index.mjs",
6
6
  "tools": [
7
7
  "synapse_health",
@@ -10,6 +10,7 @@
10
10
  "synapse_register",
11
11
  "synapse_agents_summary",
12
12
  "synapse_serve",
13
+ "synapse_serve_status",
13
14
  "synapse_emit",
14
15
  "synapse_subscribe",
15
16
  "synapse_reputation",
@@ -9,9 +9,28 @@ function eq(a, b, m) { if (a !== b) throw new Error(`${m ?? 'eq'}: expected ${JS
9
9
  function fakeConn() {
10
10
  const published = []
11
11
  const requests = new Map()
12
+ const subs = new Map()
12
13
  return {
13
14
  isClosed: () => false,
14
15
  publish(subject, data) { published.push({ subject, data: JSON.parse(new TextDecoder().decode(data)) }) },
16
+ subscribe(subject) {
17
+ const sub = {
18
+ async *[Symbol.asyncIterator]() {
19
+ while (true) {
20
+ const m = await new Promise((res) => {
21
+ const l = subs.get(subject) ?? []; subs.set(subject, l); l.push(res)
22
+ })
23
+ yield m
24
+ }
25
+ },
26
+ unsubscribe: () => subs.delete(subject),
27
+ }
28
+ return sub
29
+ },
30
+ _deliver(subject, env) {
31
+ const data = new TextEncoder().encode(JSON.stringify(env))
32
+ for (const fn of subs.get(subject) ?? []) fn({ data, subject, respond: (p) => { published.push({ subject: '_reply', env: JSON.parse(new TextDecoder().decode(p)) }) } })
33
+ },
15
34
  async request(subject, data, _opts) {
16
35
  const h = requests.get(subject)
17
36
  const reply = h ? h(JSON.parse(new TextDecoder().decode(data))) : { ok: true }
@@ -69,12 +88,12 @@ test('envelope has Synapse v0.3.0 shape', () => {
69
88
 
70
89
  // ─── register wiring ────────────────────────────────────────────────────────
71
90
 
72
- test('register wires 11 tools, 1 trigger, 1 panel (full-duplex)', () => {
91
+ test('register wires 12 tools, 1 trigger, 1 panel (full-duplex)', () => {
73
92
  const conn = fakeConn()
74
93
  const { tools, triggers, panels, ctx } = mkCtx({}, conn)
75
94
  register(ctx)
76
- eq(tools.size, 11, 'tool count')
77
- for (const n of ['synapse_health', 'synapse_discover', 'synapse_dispatch', 'synapse_register', 'synapse_agents_summary', 'synapse_serve', 'synapse_emit', 'synapse_subscribe', 'synapse_reputation', 'synapse_request_approval', 'synapse_approve']) assert(tools.has(n), `missing ${n}`)
95
+ eq(tools.size, 12, 'tool count')
96
+ for (const n of ['synapse_health', 'synapse_discover', 'synapse_dispatch', 'synapse_register', 'synapse_agents_summary', 'synapse_serve', 'synapse_serve_status', 'synapse_emit', 'synapse_subscribe', 'synapse_reputation', 'synapse_request_approval', 'synapse_approve']) assert(tools.has(n), `missing ${n}`)
78
97
  eq(triggers.length, 1, 'trigger count')
79
98
  eq(triggers[0].name, 'synapse_mesh_event', 'trigger name')
80
99
  eq(panels.length, 1, 'panel count')
@@ -83,6 +102,7 @@ test('register wires 11 tools, 1 trigger, 1 panel (full-duplex)', () => {
83
102
  // ─── discover ───────────────────────────────────────────────────────────────
84
103
 
85
104
  test('synapse_discover returns agents from registry', async () => {
105
+ __setConnForTest(null)
86
106
  const conn = fakeConn()
87
107
  conn._on('mesh.registry.discover', () => [
88
108
  { id: 'grip-cli-001', name: 'Grip CLI', skills: [{ id: 'himalaya' }] },
@@ -206,6 +226,50 @@ test('a failed connect is not cached — the next call retries', async () => {
206
226
  if (attempts !== 2) throw new Error(`expected 2 attempts (fail + retry), got ${attempts}`)
207
227
  })
208
228
 
229
+ // ─── auto-start responder on boot (serveSkills + autoServe) ─────────────────
230
+
231
+ test('autoServe: register() auto-starts the responder when enabled+autoServe (default skills)', async () => {
232
+ __setConnForTest(null)
233
+ const conn = fakeConn()
234
+ conn._on('mesh.registry.discover', () => [])
235
+ const { tools, logs, ctx } = mkCtx({}, conn)
236
+ register(ctx)
237
+ // auto-start is async (serveSkills → connectMesh → startResponder) — wait for it to settle
238
+ await new Promise((r) => setTimeout(r, 100))
239
+ const r = await tools.get('synapse_serve_status').handler({})
240
+ eq(r.serving, true, 'responder auto-started (serving=true)')
241
+ assert(r.skills.includes('status'), 'default skills include status')
242
+ assert(r.skills.includes('discover'), 'default skills include discover')
243
+ eq(r.autoServe, true, 'autoServe true by default')
244
+ })
245
+
246
+ test('autoServe: disabled when synapse.autoServe=false (no auto-start)', async () => {
247
+ __setConnForTest(null)
248
+ const conn = fakeConn()
249
+ const { tools, ctx } = mkCtx({ autoServe: false }, conn)
250
+ register(ctx)
251
+ await new Promise((r) => setTimeout(r, 20))
252
+ const r = await tools.get('synapse_serve_status').handler({})
253
+ eq(r.serving, false, 'responder NOT auto-started when autoServe=false')
254
+ })
255
+
256
+ test('autoServe: a failed auto-start does not break register (best-effort)', async () => {
257
+ __setConnForTest(null)
258
+ let failConnect = true
259
+ const conn = fakeConn()
260
+ const ctx = {
261
+ settings: { synapse: { url: 'nats://down:4222' } },
262
+ natsConnect: async () => { if (failConnect) throw new Error('server down'); return conn },
263
+ registerTool: () => {}, registerTrigger: () => {}, registerPanel: () => {}, log: () => {},
264
+ }
265
+ // register() must not throw even though the auto-start connection fails
266
+ const { register } = await import('./index.mjs')
267
+ let threw = false
268
+ try { register(ctx) } catch { threw = true }
269
+ assert(!threw, 'register() must not throw on auto-serve connection failure')
270
+ await new Promise((r) => setTimeout(r, 20)) // let the auto-start promise reject (handled)
271
+ })
272
+
209
273
  // ─── runner ─────────────────────────────────────────────────────────────────
210
274
  async function main() {
211
275
  let pass = 0, fail = 0