fix: bundle pi dependency and directive handling
This commit is contained in:
@@ -35,7 +35,8 @@
|
|||||||
"packageManager": "pnpm@10.23.0",
|
"packageManager": "pnpm@10.23.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||||
"@mariozechner/pi-coding-agent": "^0.12.4",
|
"@mariozechner/pi-ai": "^0.12.11",
|
||||||
|
"@mariozechner/pi-coding-agent": "^0.12.11",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
|
|||||||
@@ -131,7 +131,11 @@ describe("runCommandReply (pi)", () => {
|
|||||||
command: ["pi", "{{Body}}"],
|
command: ["pi", "{{Body}}"],
|
||||||
agent: { kind: "pi" },
|
agent: { kind: "pi" },
|
||||||
},
|
},
|
||||||
templatingCtx: { ...noopTemplateCtx, Body: "hello", BodyStripped: "hello" },
|
templatingCtx: {
|
||||||
|
...noopTemplateCtx,
|
||||||
|
Body: "hello",
|
||||||
|
BodyStripped: "hello",
|
||||||
|
},
|
||||||
sendSystemOnce: false,
|
sendSystemOnce: false,
|
||||||
isNewSession: true,
|
isNewSession: true,
|
||||||
isFirstTurnInSession: true,
|
isFirstTurnInSession: true,
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ function stripRpcNoise(raw: string): string {
|
|||||||
|
|
||||||
// Keep only assistant/tool messages; drop agent_start/turn_start/user/etc.
|
// Keep only assistant/tool messages; drop agent_start/turn_start/user/etc.
|
||||||
const isAssistant = role === "assistant";
|
const isAssistant = role === "assistant";
|
||||||
const isToolRole = typeof role === "string" && role.toLowerCase().includes("tool");
|
const isToolRole =
|
||||||
|
typeof role === "string" && role.toLowerCase().includes("tool");
|
||||||
if (!isAssistant && !isToolRole) continue;
|
if (!isAssistant && !isToolRole) continue;
|
||||||
|
|
||||||
// Ignore assistant messages that have no text content (pure toolcall scaffolding).
|
// Ignore assistant messages that have no text content (pure toolcall scaffolding).
|
||||||
@@ -77,8 +78,15 @@ function extractRpcAssistantText(raw: string): string | undefined {
|
|||||||
try {
|
try {
|
||||||
const evt = JSON.parse(line) as {
|
const evt = JSON.parse(line) as {
|
||||||
type?: string;
|
type?: string;
|
||||||
message?: { role?: string; content?: Array<{ type?: string; text?: string }> };
|
message?: {
|
||||||
assistantMessageEvent?: { type?: string; delta?: string; content?: string };
|
role?: string;
|
||||||
|
content?: Array<{ type?: string; text?: string }>;
|
||||||
|
};
|
||||||
|
assistantMessageEvent?: {
|
||||||
|
type?: string;
|
||||||
|
delta?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
evt.type === "message_end" &&
|
evt.type === "message_end" &&
|
||||||
@@ -329,7 +337,7 @@ export async function runCommandReply(
|
|||||||
systemSent,
|
systemSent,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
commandRunner,
|
commandRunner: _commandRunner,
|
||||||
enqueue = enqueueCommand,
|
enqueue = enqueueCommand,
|
||||||
thinkLevel,
|
thinkLevel,
|
||||||
verboseLevel,
|
verboseLevel,
|
||||||
@@ -466,9 +474,7 @@ export async function runCommandReply(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Drive pi via RPC stdin so auto-compaction and streaming run server-side.
|
// Drive pi via RPC stdin so auto-compaction and streaming run server-side.
|
||||||
let rpcInput: string | undefined;
|
|
||||||
let rpcArgv = finalArgv;
|
let rpcArgv = finalArgv;
|
||||||
rpcInput = `${JSON.stringify({ type: "prompt", message: promptArg })}\n`;
|
|
||||||
const bodyIdx =
|
const bodyIdx =
|
||||||
promptIndex >= 0 ? promptIndex : Math.max(finalArgv.length - 1, 0);
|
promptIndex >= 0 ? promptIndex : Math.max(finalArgv.length - 1, 0);
|
||||||
rpcArgv = finalArgv.filter((_, idx) => idx !== bodyIdx);
|
rpcArgv = finalArgv.filter((_, idx) => idx !== bodyIdx);
|
||||||
@@ -522,7 +528,10 @@ export async function runCommandReply(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let lastStreamedAssistant: string | undefined;
|
let lastStreamedAssistant: string | undefined;
|
||||||
const streamAssistantFinal = (msg?: { role?: string; content?: unknown[] }) => {
|
const streamAssistantFinal = (msg?: {
|
||||||
|
role?: string;
|
||||||
|
content?: unknown[];
|
||||||
|
}) => {
|
||||||
if (!onPartialReply || msg?.role !== "assistant") return;
|
if (!onPartialReply || msg?.role !== "assistant") return;
|
||||||
const textBlocks = Array.isArray(msg.content)
|
const textBlocks = Array.isArray(msg.content)
|
||||||
? (msg.content as Array<{ type?: string; text?: string }>)
|
? (msg.content as Array<{ type?: string; text?: string }>)
|
||||||
@@ -677,18 +686,18 @@ export async function runCommandReply(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const rawStdout = stdout.trim();
|
const rawStdout = stdout.trim();
|
||||||
const rpcAssistantText = extractRpcAssistantText(stdout);
|
const rpcAssistantText = extractRpcAssistantText(stdout);
|
||||||
let mediaFromCommand: string[] | undefined;
|
let mediaFromCommand: string[] | undefined;
|
||||||
const trimmed = stripRpcNoise(rawStdout);
|
const trimmed = stripRpcNoise(rawStdout);
|
||||||
if (stderr?.trim()) {
|
if (stderr?.trim()) {
|
||||||
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const logFailure = () => {
|
const logFailure = () => {
|
||||||
const truncate = (s?: string) =>
|
const truncate = (s?: string) =>
|
||||||
s ? (s.length > 4000 ? `${s.slice(0, 4000)}…` : s) : undefined;
|
s ? (s.length > 4000 ? `${s.slice(0, 4000)}…` : s) : undefined;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
code,
|
code,
|
||||||
signal,
|
signal,
|
||||||
@@ -779,23 +788,25 @@ export async function runCommandReply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If parser gave nothing, fall back to best-effort assistant text (prefers RPC deltas).
|
// If parser gave nothing, fall back to best-effort assistant text (prefers RPC deltas).
|
||||||
const fallbackText =
|
const fallbackText =
|
||||||
rpcAssistantText ??
|
rpcAssistantText ??
|
||||||
extractRpcAssistantText(trimmed) ??
|
extractRpcAssistantText(trimmed) ??
|
||||||
extractAssistantTextLoosely(trimmed) ??
|
extractAssistantTextLoosely(trimmed) ??
|
||||||
trimmed;
|
trimmed;
|
||||||
const normalize = (s?: string) =>
|
const normalize = (s?: string) =>
|
||||||
stripStructuralPrefixes((s ?? "").trim()).toLowerCase();
|
stripStructuralPrefixes((s ?? "").trim()).toLowerCase();
|
||||||
const bodyNorm = normalize(templatingCtx.Body ?? templatingCtx.BodyStripped);
|
const bodyNorm = normalize(
|
||||||
const fallbackNorm = normalize(fallbackText);
|
templatingCtx.Body ?? templatingCtx.BodyStripped,
|
||||||
const promptEcho =
|
);
|
||||||
fallbackText &&
|
const fallbackNorm = normalize(fallbackText);
|
||||||
(fallbackText === (templatingCtx.Body ?? "") ||
|
const promptEcho =
|
||||||
fallbackText === (templatingCtx.BodyStripped ?? "") ||
|
fallbackText &&
|
||||||
(bodyNorm.length > 0 && bodyNorm === fallbackNorm));
|
(fallbackText === (templatingCtx.Body ?? "") ||
|
||||||
const safeFallbackText = promptEcho ? undefined : fallbackText;
|
fallbackText === (templatingCtx.BodyStripped ?? "") ||
|
||||||
|
(bodyNorm.length > 0 && bodyNorm === fallbackNorm));
|
||||||
|
const safeFallbackText = promptEcho ? undefined : fallbackText;
|
||||||
|
|
||||||
if (replyItems.length === 0 && safeFallbackText && !hasParsedContent) {
|
if (replyItems.length === 0 && safeFallbackText && !hasParsedContent) {
|
||||||
const { text: cleanedText, mediaUrls: mediaFound } =
|
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||||
splitMediaFromOutput(safeFallbackText);
|
splitMediaFromOutput(safeFallbackText);
|
||||||
if (cleanedText || mediaFound?.length) {
|
if (cleanedText || mediaFound?.length) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as tauRpc from "../process/tau-rpc.js";
|
import * as tauRpc from "../process/tau-rpc.js";
|
||||||
import { getReplyFromConfig, extractVerboseDirective, extractThinkDirective } from "./reply.js";
|
import {
|
||||||
|
extractThinkDirective,
|
||||||
|
extractVerboseDirective,
|
||||||
|
getReplyFromConfig,
|
||||||
|
} from "./reply.js";
|
||||||
|
|
||||||
describe("directive parsing", () => {
|
describe("directive parsing", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
|
|||||||
const ABORT_MEMORY = new Map<string, boolean>();
|
const ABORT_MEMORY = new Map<string, boolean>();
|
||||||
const SYSTEM_MARK = "⚙️";
|
const SYSTEM_MARK = "⚙️";
|
||||||
|
|
||||||
|
type ReplyConfig = NonNullable<WarelayConfig["inbound"]>["reply"];
|
||||||
|
|
||||||
export function extractThinkDirective(body?: string): {
|
export function extractThinkDirective(body?: string): {
|
||||||
cleaned: string;
|
cleaned: string;
|
||||||
thinkLevel?: ThinkLevel;
|
thinkLevel?: ThinkLevel;
|
||||||
@@ -44,8 +46,7 @@ export function extractThinkDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
// Match the longest keyword first to avoid partial captures (e.g. "/think:high").
|
// Match the longest keyword first to avoid partial captures (e.g. "/think:high")
|
||||||
// Require start of string or whitespace before "/" to avoid catching URLs.
|
|
||||||
const match = body.match(
|
const match = body.match(
|
||||||
/(?:^|\s)\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i,
|
/(?:^|\s)\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i,
|
||||||
);
|
);
|
||||||
@@ -68,8 +69,9 @@ export function extractVerboseDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
// Require start or whitespace before "/verbose" and reject "/ver*" typos.
|
const match = body.match(
|
||||||
const match = body.match(/(?:^|\s)\/v(?:erbose)?\b\s*:?\s*([a-zA-Z-]+)\b/i);
|
/(?:^|\s)\/(?:verbose|v)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i,
|
||||||
|
);
|
||||||
const verboseLevel = normalizeVerboseLevel(match?.[1]);
|
const verboseLevel = normalizeVerboseLevel(match?.[1]);
|
||||||
const cleaned = match
|
const cleaned = match
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
||||||
@@ -129,7 +131,7 @@ function stripMentions(
|
|||||||
return result.replace(/\s+/g, " ").trim();
|
return result.replace(/\s+/g, " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDefaultPiReply() {
|
function makeDefaultPiReply(): ReplyConfig {
|
||||||
const piBin = resolveBundledPiBinary() ?? "pi";
|
const piBin = resolveBundledPiBinary() ?? "pi";
|
||||||
const defaultContext =
|
const defaultContext =
|
||||||
lookupContextTokens(DEFAULT_MODEL) ?? DEFAULT_CONTEXT_TOKENS;
|
lookupContextTokens(DEFAULT_MODEL) ?? DEFAULT_CONTEXT_TOKENS;
|
||||||
@@ -159,7 +161,7 @@ export async function getReplyFromConfig(
|
|||||||
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
||||||
// Choose reply from config: static text or external command stdout.
|
// Choose reply from config: static text or external command stdout.
|
||||||
const cfg = configOverride ?? loadConfig();
|
const cfg = configOverride ?? loadConfig();
|
||||||
const reply = cfg.inbound?.reply ?? makeDefaultPiReply();
|
const reply: ReplyConfig = cfg.inbound?.reply ?? makeDefaultPiReply();
|
||||||
const timeoutSeconds = Math.max(reply?.timeoutSeconds ?? 600, 1);
|
const timeoutSeconds = Math.max(reply?.timeoutSeconds ?? 600, 1);
|
||||||
const timeoutMs = timeoutSeconds * 1000;
|
const timeoutMs = timeoutSeconds * 1000;
|
||||||
let started = false;
|
let started = false;
|
||||||
@@ -216,7 +218,7 @@ export async function getReplyFromConfig(
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
||||||
const storePath = sessionCfg ? resolveStorePath(sessionCfg.store) : undefined;
|
const storePath = resolveStorePath(sessionCfg?.store);
|
||||||
let sessionStore: ReturnType<typeof loadSessionStore> | undefined;
|
let sessionStore: ReturnType<typeof loadSessionStore> | undefined;
|
||||||
let sessionKey: string | undefined;
|
let sessionKey: string | undefined;
|
||||||
let sessionEntry: SessionEntry | undefined;
|
let sessionEntry: SessionEntry | undefined;
|
||||||
@@ -230,9 +232,7 @@ export async function getReplyFromConfig(
|
|||||||
let persistedThinking: string | undefined;
|
let persistedThinking: string | undefined;
|
||||||
let persistedVerbose: string | undefined;
|
let persistedVerbose: string | undefined;
|
||||||
|
|
||||||
const triggerBodyNormalized = stripStructuralPrefixes(
|
const triggerBodyNormalized = stripStructuralPrefixes(ctx.Body ?? "")
|
||||||
ctx.Body ?? "",
|
|
||||||
)
|
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
@@ -299,38 +299,24 @@ export async function getReplyFromConfig(
|
|||||||
IsNewSession: isNewSession ? "true" : "false",
|
IsNewSession: isNewSession ? "true" : "false",
|
||||||
};
|
};
|
||||||
|
|
||||||
const directiveSource = stripStructuralPrefixes(
|
|
||||||
sessionCtx.BodyStripped ?? sessionCtx.Body ?? "",
|
|
||||||
);
|
|
||||||
const {
|
const {
|
||||||
cleaned: thinkCleanedDirective,
|
cleaned: thinkCleaned,
|
||||||
thinkLevel: inlineThink,
|
thinkLevel: inlineThink,
|
||||||
rawLevel: rawThinkLevel,
|
rawLevel: rawThinkLevel,
|
||||||
hasDirective: hasThinkDirective,
|
hasDirective: hasThinkDirective,
|
||||||
} = extractThinkDirective(directiveSource);
|
} = extractThinkDirective(sessionCtx.BodyStripped ?? sessionCtx.Body ?? "");
|
||||||
const {
|
const {
|
||||||
cleaned: verboseCleanedDirective,
|
cleaned: verboseCleaned,
|
||||||
verboseLevel: inlineVerbose,
|
verboseLevel: inlineVerbose,
|
||||||
rawLevel: rawVerboseLevel,
|
rawLevel: rawVerboseLevel,
|
||||||
hasDirective: hasVerboseDirective,
|
hasDirective: hasVerboseDirective,
|
||||||
} = extractVerboseDirective(thinkCleanedDirective);
|
} = extractVerboseDirective(thinkCleaned);
|
||||||
|
sessionCtx.Body = verboseCleaned;
|
||||||
// Keep the full body (including context wrapper) for the agent, but strip
|
sessionCtx.BodyStripped = verboseCleaned;
|
||||||
// directives from it separately so history remains intact.
|
|
||||||
const { cleaned: thinkCleanedFull } = extractThinkDirective(
|
|
||||||
sessionCtx.Body ?? "",
|
|
||||||
);
|
|
||||||
const { cleaned: verboseCleanedFull } = extractVerboseDirective(
|
|
||||||
thinkCleanedFull,
|
|
||||||
);
|
|
||||||
|
|
||||||
sessionCtx.Body = verboseCleanedFull;
|
|
||||||
sessionCtx.BodyStripped = verboseCleanedFull;
|
|
||||||
|
|
||||||
const isGroup =
|
const isGroup =
|
||||||
typeof ctx.From === "string" &&
|
typeof ctx.From === "string" &&
|
||||||
(ctx.From.includes("@g.us") || ctx.From.startsWith("group:"));
|
(ctx.From.includes("@g.us") || ctx.From.startsWith("group:"));
|
||||||
const isHeartbeat = opts?.isHeartbeat === true;
|
|
||||||
|
|
||||||
let resolvedThinkLevel =
|
let resolvedThinkLevel =
|
||||||
inlineThink ??
|
inlineThink ??
|
||||||
@@ -346,26 +332,26 @@ export async function getReplyFromConfig(
|
|||||||
hasThinkDirective &&
|
hasThinkDirective &&
|
||||||
hasVerboseDirective &&
|
hasVerboseDirective &&
|
||||||
(() => {
|
(() => {
|
||||||
const stripped = stripStructuralPrefixes(verboseCleanedDirective ?? "");
|
const stripped = stripStructuralPrefixes(verboseCleaned ?? "");
|
||||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
||||||
return noMentions.length === 0;
|
return noMentions.length === 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const directiveOnly = (() => {
|
const directiveOnly = (() => {
|
||||||
if (!hasThinkDirective) return false;
|
if (!hasThinkDirective) return false;
|
||||||
if (!thinkCleanedDirective) return true;
|
if (!thinkCleaned) return true;
|
||||||
// Check after stripping both think and verbose so combined directives count.
|
// Check after stripping both think and verbose so combined directives count.
|
||||||
const stripped = stripStructuralPrefixes(verboseCleanedDirective);
|
const stripped = stripStructuralPrefixes(verboseCleaned);
|
||||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
||||||
return noMentions.length === 0;
|
return noMentions.length === 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Directive-only message => persist session thinking level and return ack
|
// Directive-only message => persist session thinking level and return ack
|
||||||
if (!isHeartbeat && (directiveOnly || combinedDirectiveOnly)) {
|
if (directiveOnly || combinedDirectiveOnly) {
|
||||||
if (!inlineThink) {
|
if (!inlineThink) {
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return {
|
return {
|
||||||
text: `${SYSTEM_MARK} Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`,
|
text: `Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
@@ -414,24 +400,24 @@ export async function getReplyFromConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ack = `${SYSTEM_MARK} ${parts.join(" ")}`;
|
const ack = parts.join(" ");
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return { text: ack };
|
return { text: ack };
|
||||||
}
|
}
|
||||||
|
|
||||||
const verboseDirectiveOnly = (() => {
|
const verboseDirectiveOnly = (() => {
|
||||||
if (!hasVerboseDirective) return false;
|
if (!hasVerboseDirective) return false;
|
||||||
if (!verboseCleanedDirective) return true;
|
if (!verboseCleaned) return true;
|
||||||
const stripped = stripStructuralPrefixes(verboseCleanedDirective);
|
const stripped = stripStructuralPrefixes(verboseCleaned);
|
||||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped;
|
||||||
return noMentions.length === 0;
|
return noMentions.length === 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (!isHeartbeat && verboseDirectiveOnly) {
|
if (verboseDirectiveOnly) {
|
||||||
if (!inlineVerbose) {
|
if (!inlineVerbose) {
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return {
|
return {
|
||||||
text: `${SYSTEM_MARK} Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
|
text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
@@ -452,29 +438,29 @@ export async function getReplyFromConfig(
|
|||||||
return { text: ack };
|
return { text: ack };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If directives are inline with other text: persist levels, then continue to agent (no early ack).
|
// Persist inline think/verbose settings even when additional content follows.
|
||||||
if (hasThinkDirective || hasVerboseDirective) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
let updated = false;
|
||||||
if (hasThinkDirective && inlineThink) {
|
if (hasThinkDirective && inlineThink) {
|
||||||
if (inlineThink === "off") {
|
if (inlineThink === "off") {
|
||||||
delete sessionEntry.thinkingLevel;
|
delete sessionEntry.thinkingLevel;
|
||||||
} else {
|
} else {
|
||||||
sessionEntry.thinkingLevel = inlineThink;
|
sessionEntry.thinkingLevel = inlineThink;
|
||||||
}
|
|
||||||
sessionEntry.updatedAt = Date.now();
|
|
||||||
}
|
}
|
||||||
if (hasVerboseDirective && inlineVerbose) {
|
updated = true;
|
||||||
if (inlineVerbose === "off") {
|
}
|
||||||
delete sessionEntry.verboseLevel;
|
if (hasVerboseDirective && inlineVerbose) {
|
||||||
} else {
|
if (inlineVerbose === "off") {
|
||||||
sessionEntry.verboseLevel = inlineVerbose;
|
delete sessionEntry.verboseLevel;
|
||||||
}
|
} else {
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.verboseLevel = inlineVerbose;
|
||||||
}
|
|
||||||
if (sessionEntry.updatedAt) {
|
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
|
||||||
await saveSessionStore(storePath, sessionStore);
|
|
||||||
}
|
}
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
sessionEntry.updatedAt = Date.now();
|
||||||
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +525,7 @@ export async function getReplyFromConfig(
|
|||||||
const isFirstTurnInSession = isNewSession || !systemSent;
|
const isFirstTurnInSession = isNewSession || !systemSent;
|
||||||
const sessionIntro =
|
const sessionIntro =
|
||||||
isFirstTurnInSession && sessionCfg?.sessionIntro
|
isFirstTurnInSession && sessionCfg?.sessionIntro
|
||||||
? applyTemplate(sessionCfg.sessionIntro, sessionCtx)
|
? applyTemplate(sessionCfg.sessionIntro ?? "", sessionCtx)
|
||||||
: "";
|
: "";
|
||||||
const groupIntro =
|
const groupIntro =
|
||||||
isFirstTurnInSession && sessionCtx.ChatType === "group"
|
isFirstTurnInSession && sessionCtx.ChatType === "group"
|
||||||
@@ -561,7 +547,7 @@ export async function getReplyFromConfig(
|
|||||||
})()
|
})()
|
||||||
: "";
|
: "";
|
||||||
const bodyPrefix = reply?.bodyPrefix
|
const bodyPrefix = reply?.bodyPrefix
|
||||||
? applyTemplate(reply.bodyPrefix, sessionCtx)
|
? applyTemplate(reply.bodyPrefix ?? "", sessionCtx)
|
||||||
: "";
|
: "";
|
||||||
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
|
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
|
||||||
const abortedHint =
|
const abortedHint =
|
||||||
@@ -659,13 +645,15 @@ export async function getReplyFromConfig(
|
|||||||
await onReplyStart();
|
await onReplyStart();
|
||||||
logVerbose("Using text auto-reply from config");
|
logVerbose("Using text auto-reply from config");
|
||||||
const result = {
|
const result = {
|
||||||
text: applyTemplate(reply.text, templatingCtx),
|
text: applyTemplate(reply.text ?? "", templatingCtx),
|
||||||
mediaUrl: reply.mediaUrl,
|
mediaUrl: reply.mediaUrl,
|
||||||
};
|
};
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isHeartbeat = opts?.isHeartbeat === true;
|
||||||
|
|
||||||
if (reply && reply.mode === "command") {
|
if (reply && reply.mode === "command") {
|
||||||
const heartbeatCommand = isHeartbeat
|
const heartbeatCommand = isHeartbeat
|
||||||
? (reply as { heartbeatCommand?: string[] }).heartbeatCommand
|
? (reply as { heartbeatCommand?: string[] }).heartbeatCommand
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export type TemplateContext = MsgContext & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Simple {{Placeholder}} interpolation using inbound message context.
|
// Simple {{Placeholder}} interpolation using inbound message context.
|
||||||
export function applyTemplate(str: string, ctx: TemplateContext) {
|
export function applyTemplate(str: string | undefined, ctx: TemplateContext) {
|
||||||
|
if (!str) return "";
|
||||||
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
|
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
|
||||||
const value = (ctx as Record<string, unknown>)[key];
|
const value = (ctx as Record<string, unknown>)[key];
|
||||||
return value == null ? "" : String(value);
|
return value == null ? "" : String(value);
|
||||||
|
|||||||
@@ -83,20 +83,19 @@ export async function sessionsCommand(
|
|||||||
);
|
);
|
||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
|
|
||||||
const activeMinutes = opts.active
|
let activeMinutes: number | undefined;
|
||||||
? Number.parseInt(String(opts.active), 10)
|
if (opts.active !== undefined) {
|
||||||
: undefined;
|
const parsed = Number.parseInt(String(opts.active), 10);
|
||||||
if (
|
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||||
opts.active !== undefined &&
|
runtime.error("--active must be a positive integer (minutes)");
|
||||||
(Number.isNaN(activeMinutes) || activeMinutes <= 0)
|
runtime.exit(1);
|
||||||
) {
|
return;
|
||||||
runtime.error("--active must be a positive integer (minutes)");
|
}
|
||||||
runtime.exit(1);
|
activeMinutes = parsed;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = toRows(store).filter((row) => {
|
const rows = toRows(store).filter((row) => {
|
||||||
if (!activeMinutes) return true;
|
if (activeMinutes === undefined) return true;
|
||||||
if (!row.updatedAt) return false;
|
if (!row.updatedAt) return false;
|
||||||
return Date.now() - row.updatedAt <= activeMinutes * 60_000;
|
return Date.now() - row.updatedAt <= activeMinutes * 60_000;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ class TauRpcClient {
|
|||||||
this.pending.timer = setTimeout(() => {
|
this.pending.timer = setTimeout(() => {
|
||||||
const pending = this.pending;
|
const pending = this.pending;
|
||||||
this.pending = undefined;
|
this.pending = undefined;
|
||||||
pending?.reject(new Error(`tau rpc timed out after ${Math.round(capMs / 1000)}s`));
|
pending?.reject(
|
||||||
|
new Error(`tau rpc timed out after ${Math.round(capMs / 1000)}s`),
|
||||||
|
);
|
||||||
this.child?.kill("SIGKILL");
|
this.child?.kill("SIGKILL");
|
||||||
}, capMs);
|
}, capMs);
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,9 @@ class TauRpcClient {
|
|||||||
const capMs = Math.min(timeoutMs, 5 * 60 * 1000);
|
const capMs = Math.min(timeoutMs, 5 * 60 * 1000);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
this.pending = undefined;
|
this.pending = undefined;
|
||||||
reject(new Error(`tau rpc timed out after ${Math.round(capMs / 1000)}s`));
|
reject(
|
||||||
|
new Error(`tau rpc timed out after ${Math.round(capMs / 1000)}s`),
|
||||||
|
);
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}, capMs);
|
}, capMs);
|
||||||
this.pending = { resolve, reject, timer, onEvent, capMs };
|
this.pending = { resolve, reject, timer, onEvent, capMs };
|
||||||
|
|||||||
Reference in New Issue
Block a user