@securecoms/agent-runtime 0.9.2 → 0.9.4
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/README.md +1 -1
- package/dist/bin.js +258 -49
- package/package.json +3 -2
- package/skills/sql_query/SKILL.md +7 -1
package/README.md
CHANGED
|
@@ -286,7 +286,7 @@ to a host the operator did not name.
|
|
|
286
286
|
| `fetch_url` | `url` | Fetches a page and returns extracted text (HTML is stripped to text). Up to 3 redirects, 10 s timeout, 2 MB response cap. |
|
|
287
287
|
| `web_search` | `query`, `count` (1–10, default 5) | `requires: [search_provider]` — needs a readable, non-empty `SECURECOMS_BRAVE_KEY_FILE` or `SECURECOMS_TAVILY_KEY_FILE`; Brave is used first when both are set. |
|
|
288
288
|
| `http_request` | `name` (a configured `endpoint:<name>`), `path` (a query string is kept, a `#fragment` is dropped), `method` (GET/POST/PUT/DELETE), `body` | `requires: [any_endpoint]`. The model names an endpoint and a path, never a host; a path that would leave the endpoint's `baseUrl` origin is refused. |
|
|
289
|
-
| `sql_query` | `name` (a configured `database:<name>`), `sql` (read-only `SELECT` only; a `;` or a write keyword anywhere in the text — inside a string literal or a comment included — is refused) | `requires: [any_database]`. SQLite or Postgres (`pg` required separately for Postgres). Results capped at 200 rows. |
|
|
289
|
+
| `sql_query` | `name` (a configured `database:<name>`), `sql` (read-only `SELECT` only; a `;` or a write keyword anywhere in the text — inside a string literal or a comment included — is refused) | `requires: [any_database]`. SQLite or Postgres (`pg` required separately for Postgres). Results capped at 200 rows. Postgres values come back as the server's text (json/jsonb as a JSON string, arrays like `{1,2}`, timestamps, int8 and numeric as strings); rows past a 1 MiB value budget are dropped, and `truncated_reason` (`row_limit` or `size_limit`) says why rows are missing. |
|
|
290
290
|
|
|
291
291
|
## skills.json
|
|
292
292
|
|
package/dist/bin.js
CHANGED
|
@@ -2449,6 +2449,7 @@ var httpRequest = async (args, ctx) => {
|
|
|
2449
2449
|
// src/builtins/sql-query.ts
|
|
2450
2450
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2451
2451
|
import { spawn } from "node:child_process";
|
|
2452
|
+
import { createConnection } from "node:net";
|
|
2452
2453
|
|
|
2453
2454
|
// src/sql-guard.ts
|
|
2454
2455
|
var SqlNotReadOnlyError = class extends Error {
|
|
@@ -2641,15 +2642,181 @@ function executeSqliteQuery(path, sql, options = {}) {
|
|
|
2641
2642
|
}));
|
|
2642
2643
|
});
|
|
2643
2644
|
}
|
|
2644
|
-
|
|
2645
|
+
var PG_WIRE_BYTE_CAP = 4 * 1024 * 1024;
|
|
2646
|
+
var PG_ROW_BYTE_BUDGET = SQLITE_IPC_CAP_BYTES;
|
|
2647
|
+
var PG_IDLE_IN_TRANSACTION_MS = 15e3;
|
|
2648
|
+
var PG_CURSOR = "sc_q";
|
|
2649
|
+
var PG_SCALAR_PARSERS = {
|
|
2650
|
+
16: (t) => t === "t",
|
|
2651
|
+
// bool
|
|
2652
|
+
21: Number,
|
|
2653
|
+
// int2
|
|
2654
|
+
23: Number,
|
|
2655
|
+
// int4
|
|
2656
|
+
26: Number,
|
|
2657
|
+
// oid
|
|
2658
|
+
700: Number,
|
|
2659
|
+
// float4
|
|
2660
|
+
701: Number
|
|
2661
|
+
// float8
|
|
2662
|
+
};
|
|
2663
|
+
var PG_TEXT_TYPES = {
|
|
2664
|
+
getTypeParser(oid, format) {
|
|
2665
|
+
if (format === "binary") return (value) => value;
|
|
2666
|
+
return PG_SCALAR_PARSERS[oid] ?? ((value) => value);
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
var PgWireByteCapError = class extends Error {
|
|
2670
|
+
constructor() {
|
|
2671
|
+
super("database result exceeded the size limit");
|
|
2672
|
+
this.name = "PgWireByteCapError";
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
var PgWireBoundUnavailableError = class extends Error {
|
|
2676
|
+
constructor() {
|
|
2677
|
+
super(
|
|
2678
|
+
'the installed "pg" driver does not expose its socket, so the result size cannot be bounded; the query was not run'
|
|
2679
|
+
);
|
|
2680
|
+
this.name = "PgWireBoundUnavailableError";
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
var PgDriverMissingError = class extends Error {
|
|
2684
|
+
};
|
|
2685
|
+
function isPgStream(value) {
|
|
2686
|
+
return typeof value === "object" && value !== null && typeof value.on === "function" && typeof value.destroy === "function";
|
|
2687
|
+
}
|
|
2688
|
+
async function loadInstalledPg() {
|
|
2689
|
+
return import("pg");
|
|
2690
|
+
}
|
|
2691
|
+
function rowValueBytes(row) {
|
|
2692
|
+
let n = 0;
|
|
2693
|
+
for (const v of row) n += typeof v === "string" ? Buffer.byteLength(v) : 8;
|
|
2694
|
+
return n;
|
|
2695
|
+
}
|
|
2696
|
+
function sendCancelRequest(client) {
|
|
2697
|
+
const { processID, secretKey, host, port } = client;
|
|
2698
|
+
if (typeof processID !== "number" || typeof secretKey !== "number") return;
|
|
2699
|
+
if (typeof host !== "string" || typeof port !== "number") return;
|
|
2700
|
+
const packet = Buffer.alloc(16);
|
|
2701
|
+
packet.writeInt32BE(16, 0);
|
|
2702
|
+
packet.writeInt32BE(80877102, 4);
|
|
2703
|
+
packet.writeInt32BE(processID, 8);
|
|
2704
|
+
packet.writeInt32BE(secretKey, 12);
|
|
2705
|
+
const socket = host.startsWith("/") ? createConnection(`${host}/.s.PGSQL.${port}`) : createConnection(port, host);
|
|
2706
|
+
socket.setTimeout(PG_CANCEL_TIMEOUT_MS, () => socket.destroy());
|
|
2707
|
+
socket.on("error", () => socket.destroy());
|
|
2708
|
+
socket.once("connect", () => socket.end(packet));
|
|
2709
|
+
socket.unref();
|
|
2710
|
+
}
|
|
2711
|
+
var PG_CANCEL_TIMEOUT_MS = 5e3;
|
|
2712
|
+
async function executePostgresQuery(connectionString, sql, options = {}) {
|
|
2713
|
+
const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? SQL_TIMEOUT_MS));
|
|
2714
|
+
const wireByteCap = options.wireByteCap ?? PG_WIRE_BYTE_CAP;
|
|
2715
|
+
if (options.signal?.aborted) throw new Error("Postgres query cancelled");
|
|
2716
|
+
let pg;
|
|
2717
|
+
try {
|
|
2718
|
+
pg = await (options.loadDriver ?? loadInstalledPg)();
|
|
2719
|
+
} catch {
|
|
2720
|
+
throw new PgDriverMissingError('the optional "pg" module is not installed (npm i -g pg)');
|
|
2721
|
+
}
|
|
2722
|
+
const Client = pg.Client ?? pg.default?.Client;
|
|
2723
|
+
if (!Client) throw new Error(`the installed "pg" module does not export a Client`);
|
|
2724
|
+
const client = new Client({
|
|
2725
|
+
connectionString,
|
|
2726
|
+
// never logged (D4)
|
|
2727
|
+
statement_timeout: timeoutMs,
|
|
2728
|
+
connectionTimeoutMillis: timeoutMs,
|
|
2729
|
+
types: PG_TEXT_TYPES
|
|
2730
|
+
});
|
|
2731
|
+
client.on("error", () => {
|
|
2732
|
+
});
|
|
2733
|
+
let stopped;
|
|
2734
|
+
const stop = (reason) => {
|
|
2735
|
+
if (stopped) return;
|
|
2736
|
+
stopped = reason;
|
|
2737
|
+
sendCancelRequest(client);
|
|
2738
|
+
const stream = client.connection?.stream;
|
|
2739
|
+
if (isPgStream(stream)) stream.destroy();
|
|
2740
|
+
else void client.end().catch(() => {
|
|
2741
|
+
});
|
|
2742
|
+
};
|
|
2743
|
+
const onAbort = () => stop("cancelled");
|
|
2744
|
+
const deadline = setTimeout(() => stop("deadline"), timeoutMs);
|
|
2745
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2746
|
+
const stoppedError = () => {
|
|
2747
|
+
if (stopped === "capped") return new PgWireByteCapError();
|
|
2748
|
+
if (stopped === "deadline") return new Error(`Postgres query timed out after ${timeoutMs}ms`);
|
|
2749
|
+
if (stopped === "cancelled") return new Error("Postgres query cancelled");
|
|
2750
|
+
return void 0;
|
|
2751
|
+
};
|
|
2752
|
+
let began = false;
|
|
2753
|
+
try {
|
|
2754
|
+
try {
|
|
2755
|
+
await client.connect();
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
throw stoppedError() ?? error;
|
|
2758
|
+
}
|
|
2759
|
+
const early = stoppedError();
|
|
2760
|
+
if (early) throw early;
|
|
2761
|
+
const stream = client.connection?.stream;
|
|
2762
|
+
if (!isPgStream(stream)) throw new PgWireBoundUnavailableError();
|
|
2763
|
+
let wireBytes = 0;
|
|
2764
|
+
stream.on("data", (chunk) => {
|
|
2765
|
+
if (stopped) return;
|
|
2766
|
+
wireBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
2767
|
+
if (wireBytes > wireByteCap) stop("capped");
|
|
2768
|
+
});
|
|
2769
|
+
const run = async (text, rowMode) => {
|
|
2770
|
+
try {
|
|
2771
|
+
const result = await client.query(rowMode ? { text, rowMode } : text);
|
|
2772
|
+
const late = stoppedError();
|
|
2773
|
+
if (late) throw late;
|
|
2774
|
+
return result;
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
throw stoppedError() ?? error;
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
began = true;
|
|
2780
|
+
await run("BEGIN READ ONLY");
|
|
2781
|
+
await run(`SET LOCAL statement_timeout = ${timeoutMs}`);
|
|
2782
|
+
await run(`SET LOCAL idle_in_transaction_session_timeout = ${Math.max(timeoutMs, PG_IDLE_IN_TRANSACTION_MS)}`);
|
|
2783
|
+
await run(`DECLARE ${PG_CURSOR} NO SCROLL CURSOR FOR ${sql}`);
|
|
2784
|
+
const fetched = await run(`FETCH FORWARD ${SQL_ROW_CAP + 1} FROM ${PG_CURSOR}`, "array");
|
|
2785
|
+
const columns = fetched.fields.map((f) => f.name);
|
|
2786
|
+
const all = fetched.rows;
|
|
2787
|
+
let truncatedReason = all.length > SQL_ROW_CAP ? "row_limit" : void 0;
|
|
2788
|
+
const rows = [];
|
|
2789
|
+
let bytes = 0;
|
|
2790
|
+
for (const row of all.slice(0, SQL_ROW_CAP)) {
|
|
2791
|
+
bytes += rowValueBytes(row);
|
|
2792
|
+
if (bytes > PG_ROW_BYTE_BUDGET) {
|
|
2793
|
+
truncatedReason = "size_limit";
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
rows.push(row);
|
|
2797
|
+
}
|
|
2798
|
+
return { columns, rows, truncated: truncatedReason !== void 0, truncatedReason };
|
|
2799
|
+
} finally {
|
|
2800
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2801
|
+
if (began && !stopped) await client.query("ROLLBACK").catch(() => {
|
|
2802
|
+
});
|
|
2803
|
+
if (stopped) void client.end().catch(() => {
|
|
2804
|
+
});
|
|
2805
|
+
else await client.end().catch(() => {
|
|
2806
|
+
});
|
|
2807
|
+
clearTimeout(deadline);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
function envelope(columns, rows, truncated, reason) {
|
|
2645
2811
|
let kept = rows;
|
|
2646
|
-
let
|
|
2812
|
+
let why = truncated ? reason ?? "row_limit" : void 0;
|
|
2813
|
+
const render = () => JSON.stringify(why ? { columns, rows: kept, truncated: true, truncated_reason: why } : { columns, rows: kept, truncated: false });
|
|
2647
2814
|
try {
|
|
2648
|
-
let out =
|
|
2815
|
+
let out = render();
|
|
2649
2816
|
while (resultBytes(out) > RESULT_CAP && kept.length > 0) {
|
|
2650
2817
|
kept = kept.slice(0, Math.floor(kept.length / 2));
|
|
2651
|
-
|
|
2652
|
-
out =
|
|
2818
|
+
why = "size_limit";
|
|
2819
|
+
out = render();
|
|
2653
2820
|
}
|
|
2654
2821
|
if (resultBytes(out) <= RESULT_CAP) return out;
|
|
2655
2822
|
} catch {
|
|
@@ -2674,46 +2841,21 @@ var sqlQuery = async (args, ctx) => {
|
|
|
2674
2841
|
try {
|
|
2675
2842
|
if ("sqlite" in db) {
|
|
2676
2843
|
const result = await executeSqliteQuery(db.sqlite, sql, { signal: ctx.signal });
|
|
2677
|
-
return envelope(result.columns, result.rows, result.truncated);
|
|
2844
|
+
return envelope(result.columns, result.rows, result.truncated, result.truncatedReason);
|
|
2678
2845
|
}
|
|
2679
|
-
let pg;
|
|
2680
2846
|
try {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
});
|
|
2694
|
-
let cancelled = ctx.signal?.aborted ?? false;
|
|
2695
|
-
const cancel = () => {
|
|
2696
|
-
cancelled = true;
|
|
2697
|
-
void client.end().catch(() => {
|
|
2698
|
-
});
|
|
2699
|
-
};
|
|
2700
|
-
if (cancelled) throw new Error("Postgres query cancelled");
|
|
2701
|
-
ctx.signal?.addEventListener("abort", cancel, { once: true });
|
|
2702
|
-
try {
|
|
2703
|
-
await client.connect();
|
|
2704
|
-
if (cancelled) throw new Error("Postgres query cancelled");
|
|
2705
|
-
await client.query("BEGIN; SET TRANSACTION READ ONLY;");
|
|
2706
|
-
const res = await client.query({ text: sql, rowMode: "array" });
|
|
2707
|
-
if (cancelled) throw new Error("Postgres query cancelled");
|
|
2708
|
-
const columns = res.fields.map((f) => f.name);
|
|
2709
|
-
const rows = res.rows.slice(0, SQL_ROW_CAP);
|
|
2710
|
-
return envelope(columns, rows, res.rows.length > SQL_ROW_CAP);
|
|
2711
|
-
} finally {
|
|
2712
|
-
ctx.signal?.removeEventListener("abort", cancel);
|
|
2713
|
-
if (!cancelled) await client.query("ROLLBACK").catch(() => {
|
|
2714
|
-
});
|
|
2715
|
-
await client.end().catch(() => {
|
|
2716
|
-
});
|
|
2847
|
+
const result = await executePostgresQuery(
|
|
2848
|
+
readFileSync5(db.postgresUrlFile, "utf8").trim(),
|
|
2849
|
+
// never logged (D4)
|
|
2850
|
+
sql,
|
|
2851
|
+
{ signal: ctx.signal }
|
|
2852
|
+
);
|
|
2853
|
+
return envelope(result.columns, result.rows, result.truncated, result.truncatedReason);
|
|
2854
|
+
} catch (e) {
|
|
2855
|
+
if (e instanceof PgDriverMissingError) {
|
|
2856
|
+
return toolError(`database "${name}" is Postgres, but ${e.message}`);
|
|
2857
|
+
}
|
|
2858
|
+
throw e;
|
|
2717
2859
|
}
|
|
2718
2860
|
} catch (e) {
|
|
2719
2861
|
return toolError(e instanceof Error ? e.message : String(e));
|
|
@@ -3340,6 +3482,49 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
3340
3482
|
import { WebSocket } from "node:http";
|
|
3341
3483
|
import { homedir as homedir3 } from "node:os";
|
|
3342
3484
|
import { join as join5 } from "node:path";
|
|
3485
|
+
|
|
3486
|
+
// src/status-reporter.ts
|
|
3487
|
+
function createStatusReporter(log, fetchImpl = fetch) {
|
|
3488
|
+
const channels = /* @__PURE__ */ new Map();
|
|
3489
|
+
async function post(endpoint, channelId, state) {
|
|
3490
|
+
try {
|
|
3491
|
+
const res = await fetchImpl(`${endpoint.httpBase}/v1/status`, {
|
|
3492
|
+
method: "POST",
|
|
3493
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${endpoint.token}` },
|
|
3494
|
+
body: JSON.stringify({ channel_id: channelId, state })
|
|
3495
|
+
});
|
|
3496
|
+
if (!res.ok) log("warn", "transport.status_failed", { state: logLabel(state), http_status: res.status });
|
|
3497
|
+
await res.body?.cancel();
|
|
3498
|
+
} catch (e) {
|
|
3499
|
+
log("warn", "transport.status_failed", { state: logLabel(state) }, e);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
async function drain(channelId, entry) {
|
|
3503
|
+
while (entry.desired !== entry.sent) {
|
|
3504
|
+
const state = entry.desired;
|
|
3505
|
+
entry.sent = state;
|
|
3506
|
+
await post(entry.endpoint, channelId, state);
|
|
3507
|
+
}
|
|
3508
|
+
entry.inFlight = false;
|
|
3509
|
+
if (channels.get(channelId) === entry) channels.delete(channelId);
|
|
3510
|
+
}
|
|
3511
|
+
return {
|
|
3512
|
+
set(endpoint, channelId, state) {
|
|
3513
|
+
let entry = channels.get(channelId);
|
|
3514
|
+
if (!entry) {
|
|
3515
|
+
entry = { desired: state, endpoint, sent: null, inFlight: false };
|
|
3516
|
+
channels.set(channelId, entry);
|
|
3517
|
+
}
|
|
3518
|
+
entry.desired = state;
|
|
3519
|
+
entry.endpoint = endpoint;
|
|
3520
|
+
if (entry.inFlight) return;
|
|
3521
|
+
entry.inFlight = true;
|
|
3522
|
+
void drain(channelId, entry);
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// src/data-plane.ts
|
|
3343
3528
|
var NO_TURN_MEMORY = Object.freeze({
|
|
3344
3529
|
recent: Object.freeze([]),
|
|
3345
3530
|
facts: Object.freeze([])
|
|
@@ -3408,6 +3593,12 @@ function parseTurnAuthorization(value) {
|
|
|
3408
3593
|
tool_grants: Object.freeze(grants)
|
|
3409
3594
|
});
|
|
3410
3595
|
}
|
|
3596
|
+
var VOICE_GUIDANCE_MAX_CHARS = 1e3;
|
|
3597
|
+
function parseVoiceGuidance(frame) {
|
|
3598
|
+
if (frame.voice !== true || typeof frame.guidance !== "string") return void 0;
|
|
3599
|
+
const g = frame.guidance.trim().slice(0, VOICE_GUIDANCE_MAX_CHARS);
|
|
3600
|
+
return g || void 0;
|
|
3601
|
+
}
|
|
3411
3602
|
var SEEN_MAX = 5e3;
|
|
3412
3603
|
var PENDING_REPLY_MAX = 1e3;
|
|
3413
3604
|
var TERMINAL_REPLY_ERRORS = /* @__PURE__ */ new Set(["agent_paused", "send_forbidden"]);
|
|
@@ -3473,6 +3664,8 @@ var SecureComsDataPlane = class {
|
|
|
3473
3664
|
seq = 0;
|
|
3474
3665
|
/** in_reply_to → timing of a reply handed to the daemon, until its `sent` frame. */
|
|
3475
3666
|
pendingReplies = /* @__PURE__ */ new Map();
|
|
3667
|
+
/** #910: working/idle for the web "thinking" strip; fire-and-forget, per-channel ordered. */
|
|
3668
|
+
status;
|
|
3476
3669
|
constructor(cfg, onMessage) {
|
|
3477
3670
|
this.identityId = cfg.identityId;
|
|
3478
3671
|
const dir = cfg.connectionsDir ?? join5(homedir3(), ".securecoms", "connections");
|
|
@@ -3492,6 +3685,7 @@ var SecureComsDataPlane = class {
|
|
|
3492
3685
|
this.onMessage = onMessage;
|
|
3493
3686
|
this.noticeReplies = cfg.noticeReplies ?? /* @__PURE__ */ new Set();
|
|
3494
3687
|
this.clock = cfg.clock ?? (() => performance.now());
|
|
3688
|
+
this.status = createStatusReporter(this.log);
|
|
3495
3689
|
}
|
|
3496
3690
|
elapsed(from, to = this.clock()) {
|
|
3497
3691
|
return Math.max(0, Math.round(to - from));
|
|
@@ -3759,6 +3953,7 @@ var SecureComsDataPlane = class {
|
|
|
3759
3953
|
let handlerMs = 0;
|
|
3760
3954
|
let sendMs = 0;
|
|
3761
3955
|
let outcome = "failed";
|
|
3956
|
+
let reportedWorking = false;
|
|
3762
3957
|
try {
|
|
3763
3958
|
if (!this.seen.has(messageId)) {
|
|
3764
3959
|
const receiptStart = this.clock();
|
|
@@ -3785,26 +3980,33 @@ var SecureComsDataPlane = class {
|
|
|
3785
3980
|
this.turns.set(ws, socketTurns);
|
|
3786
3981
|
}
|
|
3787
3982
|
socketTurns.add(turnController);
|
|
3983
|
+
const text = String(frame.text ?? "");
|
|
3984
|
+
if (text.trim()) {
|
|
3985
|
+
reportedWorking = true;
|
|
3986
|
+
this.status.set({ httpBase, token }, channelId, "working");
|
|
3987
|
+
}
|
|
3788
3988
|
const handlerStart = this.clock();
|
|
3989
|
+
const voiceGuidance = parseVoiceGuidance(frame);
|
|
3789
3990
|
const reply = await this.onMessage({
|
|
3790
|
-
text
|
|
3991
|
+
text,
|
|
3791
3992
|
senderId: String(frame.sender_id ?? ""),
|
|
3792
3993
|
channelId,
|
|
3793
3994
|
messageId,
|
|
3794
3995
|
authorization: parseTurnAuthorization(frame.authorization),
|
|
3795
3996
|
memory: parseTurnMemory(frame.memory),
|
|
3796
|
-
seq: arrival.seq
|
|
3997
|
+
seq: arrival.seq,
|
|
3998
|
+
...voiceGuidance ? { voiceGuidance } : {}
|
|
3797
3999
|
}, turnController.signal);
|
|
3798
4000
|
handlerMs = this.elapsed(handlerStart);
|
|
3799
4001
|
if (reply != null) {
|
|
3800
|
-
const
|
|
4002
|
+
const text2 = String(reply);
|
|
3801
4003
|
const sendStart = this.clock();
|
|
3802
4004
|
ws.send(JSON.stringify({
|
|
3803
4005
|
type: "send",
|
|
3804
4006
|
channel_id: channelId,
|
|
3805
|
-
text,
|
|
4007
|
+
text: text2,
|
|
3806
4008
|
in_reply_to: messageId,
|
|
3807
|
-
...this.noticeReplies.has(
|
|
4009
|
+
...this.noticeReplies.has(text2) ? { notice: true } : {}
|
|
3808
4010
|
}));
|
|
3809
4011
|
const sentAt = this.clock();
|
|
3810
4012
|
sendMs = this.elapsed(sendStart, sentAt);
|
|
@@ -3823,6 +4025,7 @@ var SecureComsDataPlane = class {
|
|
|
3823
4025
|
this.log("error", "transport.handler_failed", {}, e);
|
|
3824
4026
|
this.failTurn(ws, messageId, "turn_failed");
|
|
3825
4027
|
} finally {
|
|
4028
|
+
if (reportedWorking) this.status.set({ httpBase, token }, channelId, "idle");
|
|
3826
4029
|
if (turnController) {
|
|
3827
4030
|
const socketTurns = this.turns.get(ws);
|
|
3828
4031
|
socketTurns?.delete(turnController);
|
|
@@ -3892,6 +4095,7 @@ async function runToolCalls(toolCalls, skills, context) {
|
|
|
3892
4095
|
}
|
|
3893
4096
|
|
|
3894
4097
|
// src/turn-loop.ts
|
|
4098
|
+
var VOICE_TURN_PREFIX = "This message was spoken on a live voice call and your reply will be read aloud: plain spoken sentences, no markdown, lists, links or code. Use your tools as usual when the question needs them. ";
|
|
3895
4099
|
var DEFAULT_SYSTEM_PROMPT = "You are a SecureComs agent working inside a private, end-to-end encrypted messaging enclave for one business; the people you talk to are its staff, partners and customers. Answer questions in the chat, and keep replies to the length the question needs.";
|
|
3896
4100
|
var DEFAULT_MAX_ITERATIONS = 5;
|
|
3897
4101
|
var DEFAULT_MAX_CONCURRENT_TURNS = 4;
|
|
@@ -4313,7 +4517,12 @@ function buildHandler(deps) {
|
|
|
4313
4517
|
const memory = memoryFor(m.channelId);
|
|
4314
4518
|
memory.replace(m.memory);
|
|
4315
4519
|
const promptStart = clock();
|
|
4316
|
-
const system = {
|
|
4520
|
+
const system = {
|
|
4521
|
+
role: "system",
|
|
4522
|
+
content: m.voiceGuidance ? `${deps.systemPromptFor()}
|
|
4523
|
+
|
|
4524
|
+
${VOICE_TURN_PREFIX}${m.voiceGuidance}` : deps.systemPromptFor()
|
|
4525
|
+
};
|
|
4317
4526
|
const recallStart = clock();
|
|
4318
4527
|
const recalledText = await memory.recall(text);
|
|
4319
4528
|
live();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@securecoms/agent-runtime",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"securecoms-agent": "dist/bin.js"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"pg": ">=8"
|
|
35
|
+
"pg": ">=8.11 <9"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"pg": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/ws": "^8.5.12",
|
|
44
44
|
"esbuild": "0.24.0",
|
|
45
|
+
"pg": "~8.23.0",
|
|
45
46
|
"typescript": "^5.4.0",
|
|
46
47
|
"vitest": "^3.2.6",
|
|
47
48
|
"ws": "^8.21.0"
|
|
@@ -18,4 +18,10 @@ database the operator configured; never invent a connection string. A semicolon
|
|
|
18
18
|
or a write keyword inside a string literal or a comment is also refused, so
|
|
19
19
|
rewrite such a value out of the query (a bound-looking literal like '--' or
|
|
20
20
|
'DROP' will not pass). If a query is refused, rewrite it as a SELECT rather than
|
|
21
|
-
retrying the same statement.
|
|
21
|
+
retrying the same statement. Values come back as the database's text: a
|
|
22
|
+
Postgres json or jsonb value is a JSON string, arrays look like {1,2}, and
|
|
23
|
+
timestamps, int8 and numeric are strings. When `truncated` is true,
|
|
24
|
+
`truncated_reason` says why: `row_limit` means more rows exist than were
|
|
25
|
+
returned, and `size_limit` means rows were dropped because they were too large
|
|
26
|
+
(a single row over 1 MiB comes back as no rows with `size_limit`, not as an
|
|
27
|
+
empty result). Select fewer or narrower columns rather than retrying as is.
|