Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions extensions/findoo-deepagent-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,29 @@ import { ExpertManager } from "./src/expert-manager.js";
import { PaperEngineClient } from "./src/paper-engine-client.js";
import { TaskStore } from "./src/task-store.js";

// Gateway LLM context ≈ 16384 tokens; keep single tool response under ~15% of available budget
const MAX_RESPONSE_CHARS = 2000;

const json = (payload: unknown) => ({
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
details: payload,
});

/** json() with a hard size cap — truncates oversized responses to protect LLM context */
const jsonCapped = (payload: unknown) => {
const raw = JSON.stringify(payload, null, 2);
if (raw.length <= MAX_RESPONSE_CHARS) return json(payload);
return {
content: [
{
type: "text" as const,
text: raw.slice(0, MAX_RESPONSE_CHARS) + "\n… [truncated, use more specific queries]",
},
],
details: { _truncated: true, _originalLength: raw.length },
};
};

export default definePluginEntry({
id: "findoo-deepagent-plugin",
name: "Findoo DeepAgent",
Expand Down Expand Up @@ -251,7 +269,7 @@ export default definePluginEntry({
async execute() {
try {
const result = await client.getSkills();
return json(result);
return jsonCapped(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to fetch skills: ${msg}` });
Expand All @@ -276,7 +294,7 @@ export default definePluginEntry({
async execute() {
try {
const result = await client.listPackages();
return json(result);
return jsonCapped(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to list packages: ${msg}` });
Expand Down Expand Up @@ -305,7 +323,7 @@ export default definePluginEntry({

try {
const result = await client.getPackageMeta(packageId);
return json(result);
return jsonCapped(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to get package meta: ${msg}` });
Expand Down Expand Up @@ -387,7 +405,7 @@ export default definePluginEntry({
parameters: Type.Object({
thread_id: Type.String({ description: "线程 ID" }),
limit: Type.Optional(
Type.Number({ description: "返回条数上限(默认 100)", minimum: 1, maximum: 200 }),
Type.Number({ description: "返回条数上限(默认 5)", minimum: 1, maximum: 20 }),
),
}),

Expand All @@ -396,9 +414,13 @@ export default definePluginEntry({
if (!threadId) return json({ error: "thread_id is required" });

try {
const limit = typeof params.limit === "number" ? params.limit : 100;
const limit = typeof params.limit === "number" ? Math.min(params.limit, 20) : 5;
const messages = await client.listMessages(threadId, limit);
return json({ count: messages.length, messages });
return jsonCapped({
total: messages.length,
showing: Math.min(limit, messages.length),
messages,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to list messages: ${msg}` });
Expand Down Expand Up @@ -465,7 +487,7 @@ export default definePluginEntry({
async execute() {
try {
const result = await client.listBacktests();
return json(result);
return jsonCapped(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to list backtests: ${msg}` });
Expand Down Expand Up @@ -496,7 +518,7 @@ export default definePluginEntry({

try {
const result = await client.getBacktestResult(taskId);
return json(result);
return jsonCapped(result);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return json({ error: `Failed to get backtest result: ${msg}` });
Expand Down
80 changes: 25 additions & 55 deletions extensions/findoo-deepagent-plugin/src/stream-relay.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/**
* Findoo SSE Stream Relay
*
* Consumes Findoo DeepAgent SSE events and relays progress/completion
* Consumes Findoo DeepAgent SSE events and relays key milestones
* to the user via SystemEvent + HeartbeatWake.
*
* Adapted from findoo-alpha-plugin's stream-relay.ts for Findoo SSE format.
* Design: push key state transitions only (TOOL_START, AGENT_HANDOFF,
* RUN_FINISHED). TEXT_DELTA is accumulated silently — full result
* available on-demand via fin_deepagent_messages.
*/

import type { FindooSSEEvent } from "./types.js";

const STREAM_SNIPPET_MAX_CHARS = 300;
const DEFAULT_STREAM_FLUSH_MS = 3_000;
// DeepAgent output ≈ 16384 tokens max; ~1.5 chars/token for Chinese = ~24K chars
const RESULT_SUMMARY_MAX_CHARS = 24_000;
const DEFAULT_NO_OUTPUT_NOTICE_MS = 60_000;
const DEFAULT_MAX_RELAY_LIFETIME_MS = 600_000; // 10 min

Expand All @@ -32,7 +34,6 @@ export type StreamRelayConfig = {
enqueueSystemEvent: (text: string, options: { sessionKey: string; contextKey?: string }) => void;
requestHeartbeatNow: (options?: { reason?: string; sessionKey?: string }) => void;
logger?: { info: (msg: string) => void };
streamFlushMs?: number;
noOutputNoticeMs?: number;
maxRelayLifetimeMs?: number;
};
Expand All @@ -46,13 +47,10 @@ export function startStreamRelay(
stream: AsyncIterable<FindooSSEEvent>,
config: StreamRelayConfig,
): StreamRelayHandle {
const flushMs = config.streamFlushMs ?? DEFAULT_STREAM_FLUSH_MS;
const noOutputMs = config.noOutputNoticeMs ?? DEFAULT_NO_OUTPUT_NOTICE_MS;
const maxLifetimeMs = config.maxRelayLifetimeMs ?? DEFAULT_MAX_RELAY_LIFETIME_MS;

let aborted = false;
let pendingText = "";
let flushTimer: ReturnType<typeof setTimeout> | undefined;
let lastOutputAt = Date.now();

const contextKeyPrefix = `findoo:deepagent:${config.taskId}`;
Expand All @@ -69,27 +67,13 @@ export function startStreamRelay(
config.logger?.info(`findoo-deepagent: relay emit [${suffix}] (${text.length} chars)`);
}

function flushPending() {
if (flushTimer) {
clearTimeout(flushTimer);
flushTimer = undefined;
}
if (!pendingText || aborted) return;

let snippet = pendingText.replace(/\s+/g, " ").trim();
if (snippet.length > STREAM_SNIPPET_MAX_CHARS) {
snippet = snippet.slice(0, STREAM_SNIPPET_MAX_CHARS) + "…";
}
pendingText = "";

if (snippet) {
emitEvent(`${config.productName} ${config.label}进度:${snippet}`, "progress");
}
}

function scheduleFlush() {
if (flushTimer || aborted) return;
flushTimer = setTimeout(flushPending, flushMs);
/** Truncate full result to a compact summary for SystemEvent push */
function truncateResult(text: string): string {
if (!text.trim()) return "分析已完成";
if (text.length <= RESULT_SUMMARY_MAX_CHARS) return text;
return (
text.slice(0, RESULT_SUMMARY_MAX_CHARS) + "…\n(完整结果可通过 fin_deepagent_messages 查看)"
);
}

const done = new Promise<{
Expand All @@ -98,22 +82,23 @@ export function startStreamRelay(
}>((resolve) => {
const lifetimeTimer = setTimeout(() => {
aborted = true;
flushPending();
emitEvent(
`${config.productName} ${config.label}超时(超过${Math.round(maxLifetimeMs / 60_000)}分钟),请稍后重试。`,
"error",
);
resolve({ status: "timeout" });
}, maxLifetimeMs);

// Stall checker — emit at most once per task
let stallEmitted = false;
const stallChecker = setInterval(() => {
if (aborted) {
clearInterval(stallChecker);
return;
}
if (Date.now() - lastOutputAt > noOutputMs) {
if (!stallEmitted && Date.now() - lastOutputAt > noOutputMs) {
emitEvent(`${config.productName} ${config.label}正在处理中,请耐心等待…`, "stall");
lastOutputAt = Date.now();
stallEmitted = true;
}
}, noOutputMs / 2);

Expand All @@ -127,30 +112,22 @@ export function startStreamRelay(

switch (event.event) {
case "RUN_STARTED":
// Run started — no user-facing action needed
break;

case "TEXT_DELTA":
pendingText += event.data.delta;
// Accumulate only — no SystemEvent push for incremental text.
// Key milestone events (TOOL_START, AGENT_HANDOFF) handle progress.
finalText += event.data.delta;

if (pendingText.length >= STREAM_SNIPPET_MAX_CHARS || pendingText.includes("\n\n")) {
flushPending();
} else {
scheduleFlush();
}
break;

case "TOOL_START":
// Emit tool activity as progress
emitEvent(
`${config.productName} ${config.label}:正在查询 ${event.data.toolName}…`,
"progress",
);
break;

case "TOOL_DONE":
// Silent — tool completed
break;

case "AGENT_HANDOFF": {
Expand All @@ -163,7 +140,6 @@ export function startStreamRelay(
}

case "ERROR":
flushPending();
emitEvent(
`${config.productName} ${config.label}出错:${event.data.error.slice(0, 200)}`,
"error",
Expand All @@ -174,7 +150,6 @@ export function startStreamRelay(
return;

case "RUN_FINISHED":
flushPending();
if (event.data.isError) {
emitEvent(
`${config.productName} ${config.label}出错:${event.data.text.slice(0, 200)}`,
Expand All @@ -184,29 +159,28 @@ export function startStreamRelay(
clearInterval(stallChecker);
resolve({ status: "failed", finalText: event.data.text });
} else {
const summary = event.data.text || finalText || "分析已完成";
emitEvent(`${config.productName} ${config.label}完成:${summary}`, "done");
const fullResult = event.data.text || finalText || "";
const summary = truncateResult(fullResult);
emitEvent(`${config.productName} ${config.label}完成:\n${summary}`, "done");
clearTimeout(lifetimeTimer);
clearInterval(stallChecker);
resolve({ status: "completed", finalText: event.data.text || finalText });
resolve({ status: "completed", finalText: fullResult });
}
return;
}
}

// Stream ended without RUN_FINISHED (unexpected)
flushPending();
if (!aborted) {
const summary = finalText ? finalText.slice(0, 500) : "分析已完成";
emitEvent(`${config.productName} ${config.label}完成:${summary}`, "done");
const summary = truncateResult(finalText);
emitEvent(`${config.productName} ${config.label}完成:\n${summary}`, "done");
}
clearTimeout(lifetimeTimer);
clearInterval(stallChecker);
resolve({ status: "completed", finalText });
} catch (err) {
if (aborted) return;
const errMsg = err instanceof Error ? err.message : String(err);
flushPending();
emitEvent(`${config.productName} ${config.label}出错:${errMsg.slice(0, 200)}`, "error");
clearTimeout(lifetimeTimer);
clearInterval(stallChecker);
Expand All @@ -219,10 +193,6 @@ export function startStreamRelay(
done,
abort() {
aborted = true;
if (flushTimer) {
clearTimeout(flushTimer);
flushTimer = undefined;
}
},
};
}
Loading