neuralos 3.8.7 → 3.8.9
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
|
@@ -346328,6 +346328,7 @@ var AgentRunLedger = class {
|
|
|
346328
346328
|
var CHECKPOINT_STRING_CAP = 8 * 1024;
|
|
346329
346329
|
var CHECKPOINT_BYTES_CAP = 16 * 1024 * 1024;
|
|
346330
346330
|
var CHECKPOINT_MAX_MESSAGES = 40;
|
|
346331
|
+
var CHECKPOINT_RETENTION = 8;
|
|
346331
346332
|
function truncateString(input, max = CHECKPOINT_STRING_CAP) {
|
|
346332
346333
|
if (input.length <= max) return input;
|
|
346333
346334
|
const omitted = input.length - max;
|
|
@@ -346395,8 +346396,18 @@ function isAllocationError(error40) {
|
|
|
346395
346396
|
var SafeMemorySaver = class extends MemorySaver {
|
|
346396
346397
|
async put(config2, checkpoint, metadata) {
|
|
346397
346398
|
const pruned = pruneCheckpointForSaver(checkpoint);
|
|
346399
|
+
const threadId = config2?.configurable?.thread_id;
|
|
346400
|
+
const ns = config2?.configurable?.checkpoint_ns ?? "";
|
|
346401
|
+
const checkpointId = checkpoint?.id;
|
|
346402
|
+
if (threadId && checkpointId) {
|
|
346403
|
+
const order = this.putOrderFor(threadId, ns);
|
|
346404
|
+
order.push(checkpointId);
|
|
346405
|
+
this.setPutOrder(threadId, ns, order.slice(-CHECKPOINT_RETENTION * 4));
|
|
346406
|
+
}
|
|
346398
346407
|
try {
|
|
346399
|
-
|
|
346408
|
+
const result = await super.put(config2, pruned, pruneValue(metadata));
|
|
346409
|
+
this.evictOldCheckpoints(config2);
|
|
346410
|
+
return result;
|
|
346400
346411
|
} catch (error40) {
|
|
346401
346412
|
if (!isAllocationError(error40)) throw error40;
|
|
346402
346413
|
console.warn(
|
|
@@ -346409,12 +346420,15 @@ var SafeMemorySaver = class extends MemorySaver {
|
|
|
346409
346420
|
channels.messages = channels.messages.slice(-8);
|
|
346410
346421
|
}
|
|
346411
346422
|
tighter.channel_values = channels;
|
|
346412
|
-
|
|
346423
|
+
const result = await super.put(config2, tighter, { source: "oom-pruned" });
|
|
346424
|
+
this.evictOldCheckpoints(config2);
|
|
346425
|
+
return result;
|
|
346413
346426
|
} catch (retryError) {
|
|
346414
346427
|
if (!isAllocationError(retryError)) throw retryError;
|
|
346415
346428
|
console.warn(
|
|
346416
346429
|
"[SafeMemorySaver] checkpoint still too large \u2014 skipping persist so the run can finish"
|
|
346417
346430
|
);
|
|
346431
|
+
this.evictOldCheckpoints(config2);
|
|
346418
346432
|
return {
|
|
346419
346433
|
configurable: {
|
|
346420
346434
|
thread_id: config2?.configurable?.thread_id,
|
|
@@ -346425,6 +346439,46 @@ var SafeMemorySaver = class extends MemorySaver {
|
|
|
346425
346439
|
}
|
|
346426
346440
|
}
|
|
346427
346441
|
}
|
|
346442
|
+
/**
|
|
346443
|
+
* Keep only the newest CHECKPOINT_RETENTION checkpoints for the thread/ns
|
|
346444
|
+
* in this config. MemorySaver stores them as
|
|
346445
|
+
* storage[threadId][ns][checkpointId] = [serializedCheckpoint, metadata,
|
|
346446
|
+
* parentId]; there is no timestamp, but checkpoint ids from langgraph are
|
|
346447
|
+
* UUIDv4-orderable-ish in practice and — more reliably — the number of
|
|
346448
|
+
* entries is what matters for memory. We cannot know insertion order from
|
|
346449
|
+
* the object shape alone, so we track it ourselves: the ids we just put
|
|
346450
|
+
* are recorded, and anything beyond the newest window is dropped.
|
|
346451
|
+
*/
|
|
346452
|
+
evictOldCheckpoints(config2) {
|
|
346453
|
+
try {
|
|
346454
|
+
const threadId = config2?.configurable?.thread_id;
|
|
346455
|
+
const ns = config2?.configurable?.checkpoint_ns ?? "";
|
|
346456
|
+
if (!threadId) return;
|
|
346457
|
+
const storage = this.storage;
|
|
346458
|
+
const nsMap = storage?.[threadId]?.[ns];
|
|
346459
|
+
if (!nsMap) return;
|
|
346460
|
+
const ids = Object.keys(nsMap);
|
|
346461
|
+
if (ids.length <= CHECKPOINT_RETENTION) return;
|
|
346462
|
+
const order = this.putOrderFor(threadId, ns);
|
|
346463
|
+
const keep = new Set(order.slice(-CHECKPOINT_RETENTION));
|
|
346464
|
+
for (const id of ids) {
|
|
346465
|
+
if (!keep.has(id) && !order.includes(id)) keep.add(id);
|
|
346466
|
+
}
|
|
346467
|
+
for (const id of ids) {
|
|
346468
|
+
if (!keep.has(id)) delete nsMap[id];
|
|
346469
|
+
}
|
|
346470
|
+
const remaining = order.filter((id) => id in nsMap);
|
|
346471
|
+
this.setPutOrder(threadId, ns, remaining.slice(-CHECKPOINT_RETENTION * 2));
|
|
346472
|
+
} catch {
|
|
346473
|
+
}
|
|
346474
|
+
}
|
|
346475
|
+
putOrders = /* @__PURE__ */ new Map();
|
|
346476
|
+
putOrderFor(threadId, ns) {
|
|
346477
|
+
return this.putOrders.get(`${threadId}\0${ns}`) ?? [];
|
|
346478
|
+
}
|
|
346479
|
+
setPutOrder(threadId, ns, ids) {
|
|
346480
|
+
this.putOrders.set(`${threadId}\0${ns}`, ids);
|
|
346481
|
+
}
|
|
346428
346482
|
async putWrites(config2, writes, taskId) {
|
|
346429
346483
|
const prunedWrites = Array.isArray(writes) ? writes.map((w) => {
|
|
346430
346484
|
if (Array.isArray(w) && w.length >= 2) {
|
|
@@ -366235,6 +366289,15 @@ var AgentHelpers = class {
|
|
|
366235
366289
|
setEventPublisher(publisher) {
|
|
366236
366290
|
this.eventPublisher = publisher;
|
|
366237
366291
|
}
|
|
366292
|
+
/**
|
|
366293
|
+
* Current event publisher (or null). Lets a runtime CHAIN onto the
|
|
366294
|
+
* publisher temporarily — e.g. the plugin runAgentTask hook captures
|
|
366295
|
+
* `say` events for one mesh-invoke session while passing everything
|
|
366296
|
+
* through to the gateway's broadcast unchanged.
|
|
366297
|
+
*/
|
|
366298
|
+
getEventPublisher() {
|
|
366299
|
+
return this.eventPublisher;
|
|
366300
|
+
}
|
|
366238
366301
|
/**
|
|
366239
366302
|
* Build a temporary history for the action model to make decisions.
|
|
366240
366303
|
* This includes the last 3 special marker messages and recent execution details.
|
|
@@ -368273,6 +368336,11 @@ var AgentService_v2 = class {
|
|
|
368273
368336
|
setEventPublisher(publisher) {
|
|
368274
368337
|
this.helpers.setEventPublisher(publisher);
|
|
368275
368338
|
}
|
|
368339
|
+
/** Current event publisher (or null) — lets a runtime chain onto it
|
|
368340
|
+
* temporarily (plugin runAgentTask hook) without replacing it. */
|
|
368341
|
+
getEventPublisher() {
|
|
368342
|
+
return this.helpers.getEventPublisher();
|
|
368343
|
+
}
|
|
368276
368344
|
/** Wire the connection manager so `manage_ssh_connection` can mutate saved
|
|
368277
368345
|
* connections + notify the UI. Optional; only needed in full runtimes. */
|
|
368278
368346
|
setConnectionManager(manager) {
|
|
@@ -371778,6 +371846,23 @@ ${reminder}`;
|
|
|
371778
371846
|
baseMessages = mapStoredMessagesToChatMessages(
|
|
371779
371847
|
sanitizedStoredMessages.messages
|
|
371780
371848
|
);
|
|
371849
|
+
const interruptedRun = this.chatHistoryService.getRunMarker(sessionId);
|
|
371850
|
+
if (interruptedRun) {
|
|
371851
|
+
const minutesAgo = Math.max(
|
|
371852
|
+
0,
|
|
371853
|
+
Math.round((Date.now() - (interruptedRun.startedAt || 0)) / 6e4)
|
|
371854
|
+
);
|
|
371855
|
+
baseMessages.push(
|
|
371856
|
+
new SystemMessage(
|
|
371857
|
+
`[INTERRUPTION NOTICE] The previous task on this session was interrupted ${minutesAgo > 0 ? `${minutesAgo} minutes ` : ""}ago (the application stopped before the response completed \u2014 likely a force quit or crash). The last assistant response above may be PARTIAL: it can end mid-sentence, mid-tool-call, or before the task was actually finished. ` + (interruptedRun.inputPreview ? `The interrupted task began with: "${interruptedRun.inputPreview}"
|
|
371858
|
+
` : "") + `Do not assume the prior task completed. If the user's next message relates to it, offer to continue or finish the interrupted work rather than starting from scratch.`
|
|
371859
|
+
)
|
|
371860
|
+
);
|
|
371861
|
+
this.chatHistoryService.clearRunMarker(sessionId);
|
|
371862
|
+
console.warn(
|
|
371863
|
+
`[AgentService_v2] Restored session ${sessionId} after an interrupted run (${interruptedRun.runId}); injected interruption notice into context.`
|
|
371864
|
+
);
|
|
371865
|
+
}
|
|
371781
371866
|
}
|
|
371782
371867
|
const runExperimentalFlags = resolveRunExperimentalFlags(
|
|
371783
371868
|
context2,
|
|
@@ -371797,6 +371882,11 @@ ${reminder}`;
|
|
|
371797
371882
|
execCommandActionModelEnabled: runExperimentalFlags.execCommandActionModelEnabled,
|
|
371798
371883
|
writeStdinActionModelEnabled: runExperimentalFlags.writeStdinActionModelEnabled
|
|
371799
371884
|
};
|
|
371885
|
+
this.chatHistoryService.setRunMarker(sessionId, {
|
|
371886
|
+
runId: ledgerRunId,
|
|
371887
|
+
startedAt: Date.now(),
|
|
371888
|
+
inputPreview: ledgerInputPreview.slice(0, 500)
|
|
371889
|
+
});
|
|
371800
371890
|
try {
|
|
371801
371891
|
const result = await this.graph.invoke(initialState, {
|
|
371802
371892
|
recursionLimit,
|
|
@@ -371855,6 +371945,7 @@ ${reminder}`;
|
|
|
371855
371945
|
this.currentRunIdBySession.delete(sessionId);
|
|
371856
371946
|
this.agentRunLedger?.finishRun(ledgerRunId, ledgerExitStatus, ledgerExitError);
|
|
371857
371947
|
void this.recordCompoundingLessons(sessionId, ledgerRunId, ledgerExitStatus, ledgerExitError);
|
|
371948
|
+
this.chatHistoryService.clearRunMarker(sessionId);
|
|
371858
371949
|
await this.clearCheckpoint(sessionId);
|
|
371859
371950
|
}
|
|
371860
371951
|
}
|
|
@@ -373426,9 +373517,11 @@ var ChatHistoryService = class {
|
|
|
373426
373517
|
saveSession(session) {
|
|
373427
373518
|
const createdAt = this.store.getChatSessionCreatedAt(session.id);
|
|
373428
373519
|
const now = Date.now();
|
|
373520
|
+
const storedMeta = this.store.getChatSessionMeta(session.id);
|
|
373521
|
+
const title = storedMeta?.title ?? session.title;
|
|
373429
373522
|
this.store.saveChatSession({
|
|
373430
373523
|
id: session.id,
|
|
373431
|
-
title
|
|
373524
|
+
title,
|
|
373432
373525
|
messages: Array.from(session.messages.entries()).map(([id, message]) => ({
|
|
373433
373526
|
id,
|
|
373434
373527
|
type: message._getType ? message._getType() : "unknown",
|
|
@@ -373448,6 +373541,32 @@ var ChatHistoryService = class {
|
|
|
373448
373541
|
getSessionMeta(sessionId) {
|
|
373449
373542
|
return this.store.getChatSessionMeta(sessionId);
|
|
373450
373543
|
}
|
|
373544
|
+
// ─── v3.8.9: run interruption markers ─────────────────────────────────────
|
|
373545
|
+
// A marker is SET at run start and CLEARED in the run's finally block.
|
|
373546
|
+
// Only a hard kill (force quit, crash, power loss) skips the finally —
|
|
373547
|
+
// so a leftover marker is a durable, restart-surviving record that the
|
|
373548
|
+
// session's previous run was interrupted. Stored in the history_meta
|
|
373549
|
+
// table (plain SQLite, survives anything short of disk loss).
|
|
373550
|
+
runMarkerKey(sessionId) {
|
|
373551
|
+
return `run-marker:${sessionId}`;
|
|
373552
|
+
}
|
|
373553
|
+
setRunMarker(sessionId, marker) {
|
|
373554
|
+
this.store.setMeta(this.runMarkerKey(sessionId), JSON.stringify(marker));
|
|
373555
|
+
}
|
|
373556
|
+
getRunMarker(sessionId) {
|
|
373557
|
+
const raw = this.store.getMeta(this.runMarkerKey(sessionId));
|
|
373558
|
+
if (!raw) return null;
|
|
373559
|
+
try {
|
|
373560
|
+
const parsed = JSON.parse(raw);
|
|
373561
|
+
if (typeof parsed?.runId === "string") return parsed;
|
|
373562
|
+
return null;
|
|
373563
|
+
} catch {
|
|
373564
|
+
return null;
|
|
373565
|
+
}
|
|
373566
|
+
}
|
|
373567
|
+
clearRunMarker(sessionId) {
|
|
373568
|
+
this.store.setMeta(this.runMarkerKey(sessionId), "");
|
|
373569
|
+
}
|
|
373451
373570
|
loadSession(sessionId) {
|
|
373452
373571
|
const storedSession = this.store.loadChatSession(sessionId);
|
|
373453
373572
|
if (!storedSession) {
|
|
@@ -392163,7 +392282,7 @@ var PluginRegistry = class {
|
|
|
392163
392282
|
}
|
|
392164
392283
|
/** Build the default PluginContext for a record (registers into the record's
|
|
392165
392284
|
* capability lists; exec/readLedger/log are delegated to the injected fns). */
|
|
392166
|
-
static defaultContext(record2, exec, readLedger, log, spawnProcess, getSettings) {
|
|
392285
|
+
static defaultContext(record2, exec, readLedger, log, spawnProcess, getSettings, runAgentTask) {
|
|
392167
392286
|
return {
|
|
392168
392287
|
registerTool: (tool2) => {
|
|
392169
392288
|
record2.tools.push(tool2);
|
|
@@ -392184,7 +392303,8 @@ var PluginRegistry = class {
|
|
|
392184
392303
|
...spawnProcess ? { spawnProcess } : {},
|
|
392185
392304
|
...getSettings ? { getSettings, get settings() {
|
|
392186
392305
|
return getSettings();
|
|
392187
|
-
} } : {}
|
|
392306
|
+
} } : {},
|
|
392307
|
+
...runAgentTask ? { runAgentTask } : {}
|
|
392188
392308
|
};
|
|
392189
392309
|
}
|
|
392190
392310
|
};
|
|
@@ -394723,7 +394843,50 @@ function createObservability(deps) {
|
|
|
394723
394843
|
});
|
|
394724
394844
|
},
|
|
394725
394845
|
// Live settings snapshot for plugins that read config blocks (webIntel, agentspan, …).
|
|
394726
|
-
() => deps.settingsService?.getSettings?.() ?? {}
|
|
394846
|
+
() => deps.settingsService?.getSettings?.() ?? {},
|
|
394847
|
+
// Real agent turns for plugins that serve an inbound `invoke` skill
|
|
394848
|
+
// (reactorpro-bridge): a mesh peer sends a prompt, RTerm runs it as a
|
|
394849
|
+
// genuine agent turn and the answer goes back over the mesh.
|
|
394850
|
+
// Chained onto the agent's event publisher for the duration of the
|
|
394851
|
+
// turn: `say` events for OUR session are captured, everything passes
|
|
394852
|
+
// through to the original (gateway) publisher unchanged, and the
|
|
394853
|
+
// publisher is restored in finally. dispatchTask resolves when the
|
|
394854
|
+
// run completes, so the last captured `say` is the final answer.
|
|
394855
|
+
deps.agentService ? async (prompt, opts) => {
|
|
394856
|
+
const agent = deps.agentService;
|
|
394857
|
+
const sessionId = opts?.sessionId ?? `mesh-invoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
394858
|
+
const timeoutMs = Math.min(Math.max(opts?.timeoutMs ?? 17e4, 5e3), 6e5);
|
|
394859
|
+
const previous = agent.getEventPublisher();
|
|
394860
|
+
const sayParts = [];
|
|
394861
|
+
const chained = (sid, ev) => {
|
|
394862
|
+
if (sid === sessionId && ev?.type === "say" && typeof ev.content === "string" && ev.content) {
|
|
394863
|
+
sayParts.push(ev.content);
|
|
394864
|
+
}
|
|
394865
|
+
if (previous) {
|
|
394866
|
+
try {
|
|
394867
|
+
previous(sid, ev);
|
|
394868
|
+
} catch {
|
|
394869
|
+
}
|
|
394870
|
+
}
|
|
394871
|
+
};
|
|
394872
|
+
agent.setEventPublisher(chained);
|
|
394873
|
+
let timer;
|
|
394874
|
+
try {
|
|
394875
|
+
await Promise.race([
|
|
394876
|
+
deps.gatewayService.dispatchTask(sessionId, prompt, void 0),
|
|
394877
|
+
new Promise((_, reject) => {
|
|
394878
|
+
timer = setTimeout(() => reject(new Error(`agent turn timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
394879
|
+
})
|
|
394880
|
+
]);
|
|
394881
|
+
} finally {
|
|
394882
|
+
if (timer) clearTimeout(timer);
|
|
394883
|
+
agent.setEventPublisher((sid, ev) => {
|
|
394884
|
+
if (previous) previous(sid, ev);
|
|
394885
|
+
});
|
|
394886
|
+
}
|
|
394887
|
+
const answer = sayParts.join("");
|
|
394888
|
+
return { ok: answer.trim() !== "", answer, sessionId };
|
|
394889
|
+
} : void 0
|
|
394727
394890
|
),
|
|
394728
394891
|
onLog: deps.onLog
|
|
394729
394892
|
});
|
package/package.json
CHANGED
|
@@ -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
|
}
|