Merge pull request #203 from multica-ai/forrestchang/finance-benchmark
fix: agent stability, tooling, and sub-agent orchestration
This commit is contained in:
commit
74c0ca0ddc
13 changed files with 294 additions and 66 deletions
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { join } from "node:path";
|
||||
import { Agent } from "@multica/core";
|
||||
import { Agent, Hub, listSubagentRuns } from "@multica/core";
|
||||
import type { AgentOptions } from "@multica/core";
|
||||
import type { ToolsConfig } from "@multica/core";
|
||||
import { DATA_DIR } from "@multica/utils";
|
||||
|
|
@ -192,35 +192,90 @@ export async function runCommand(args: string[]): Promise<void> {
|
|||
|
||||
const enableRunLog = opts.runLog || !!process.env.MULTICA_RUN_LOG;
|
||||
|
||||
const agent = new Agent({
|
||||
profileId: opts.profile,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
apiKey: opts.apiKey,
|
||||
baseUrl: opts.baseUrl,
|
||||
systemPrompt: opts.system,
|
||||
thinkingLevel: opts.thinking as any,
|
||||
reasoningMode: opts.reasoning as AgentOptions["reasoningMode"],
|
||||
cwd: opts.cwd,
|
||||
sessionId: opts.session,
|
||||
debug: opts.debug,
|
||||
enableRunLog,
|
||||
tools: toolsConfig,
|
||||
});
|
||||
// Initialize Hub to enable full agent capabilities (sub-agents, channels, cron).
|
||||
// Matches Desktop environment where Hub is always active.
|
||||
// Gateway connection failures are non-blocking (auto-reconnect with backoff).
|
||||
const gatewayUrl = process.env.GATEWAY_URL || "http://localhost:3000";
|
||||
const hub = new Hub(gatewayUrl);
|
||||
|
||||
const sessionDir = join(DATA_DIR, "sessions", agent.sessionId);
|
||||
try {
|
||||
const agent = new Agent({
|
||||
profileId: opts.profile,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
apiKey: opts.apiKey,
|
||||
baseUrl: opts.baseUrl,
|
||||
systemPrompt: opts.system,
|
||||
thinkingLevel: opts.thinking as any,
|
||||
reasoningMode: opts.reasoning as AgentOptions["reasoningMode"],
|
||||
cwd: opts.cwd,
|
||||
sessionId: opts.session,
|
||||
debug: opts.debug,
|
||||
enableRunLog,
|
||||
tools: toolsConfig,
|
||||
});
|
||||
|
||||
// If it's a newly created session, notify user of sessionId
|
||||
if (!opts.session) {
|
||||
console.error(`[session: ${agent.sessionId}]`);
|
||||
}
|
||||
if (enableRunLog) {
|
||||
console.error(`[session-dir: ${sessionDir}]`);
|
||||
}
|
||||
const sessionDir = join(DATA_DIR, "sessions", agent.sessionId);
|
||||
|
||||
const result = await agent.run(finalPrompt);
|
||||
if (result.error) {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exitCode = 1;
|
||||
// If it's a newly created session, notify user of sessionId
|
||||
if (!opts.session) {
|
||||
console.error(`[session: ${agent.sessionId}]`);
|
||||
}
|
||||
if (enableRunLog) {
|
||||
console.error(`[session-dir: ${sessionDir}]`);
|
||||
}
|
||||
|
||||
const result = await agent.run(finalPrompt);
|
||||
if (result.error) {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
// Wait for sub-agents to complete and parent to process their results.
|
||||
// Without this, CLI exits before sub-agent announcements are delivered.
|
||||
await waitForSubagents(agent);
|
||||
} finally {
|
||||
hub.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any running sub-agents to complete, then output their findings.
|
||||
*
|
||||
* In CLI mode, the parent Agent is not registered with the Hub, so the normal
|
||||
* announce flow (Hub → writeInternal) can't deliver results. Instead, we poll
|
||||
* the registry and print findings directly once all sub-agents finish.
|
||||
*
|
||||
* Max wait: 30 minutes (matches default sub-agent timeout).
|
||||
*/
|
||||
async function waitForSubagents(agent: Agent): Promise<void> {
|
||||
const MAX_WAIT_MS = 30 * 60 * 1000;
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const start = Date.now();
|
||||
|
||||
const allRuns = listSubagentRuns(agent.sessionId);
|
||||
if (allRuns.length === 0) return;
|
||||
|
||||
// Phase 1: Wait for all sub-agent runs to finish
|
||||
while (Date.now() - start < MAX_WAIT_MS) {
|
||||
const runs = listSubagentRuns(agent.sessionId);
|
||||
const running = runs.filter((r) => !r.endedAt);
|
||||
if (running.length === 0) break;
|
||||
console.error(dim(`[waiting for ${running.length} sub-agent(s)...]`));
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
}
|
||||
|
||||
// Phase 2: Output sub-agent findings directly (bypasses Hub announce flow)
|
||||
const completedRuns = listSubagentRuns(agent.sessionId).filter((r) => r.endedAt);
|
||||
if (completedRuns.length === 0) return;
|
||||
|
||||
console.error(dim(`[${completedRuns.length} sub-agent(s) completed]`));
|
||||
|
||||
for (const run of completedRuns) {
|
||||
const displayName = run.label || run.task.slice(0, 60);
|
||||
const status = run.outcome?.status ?? "unknown";
|
||||
const findings = run.findings || "(no output)";
|
||||
console.log(`\n--- Sub-agent: ${displayName} [${status}] ---`);
|
||||
console.log(findings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export type CredentialsConfig = {
|
|||
};
|
||||
|
||||
const DEFAULT_CREDENTIALS_PATH = join(DATA_DIR, "credentials.json5");
|
||||
const FALLBACK_CREDENTIALS_PATH = join(homedir(), ".super-multica", "credentials.json5");
|
||||
|
||||
function expandHome(value: string): string {
|
||||
if (value === "~") return homedir();
|
||||
|
|
@ -53,9 +54,34 @@ function isTestEnv(): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the credentials file path.
|
||||
*
|
||||
* Lookup order:
|
||||
* 1. SMC_CREDENTIALS_PATH env var (explicit override)
|
||||
* 2. {DATA_DIR}/credentials.json5 (current data dir, respects SMC_DATA_DIR)
|
||||
* 3. ~/.super-multica/credentials.json5 (default location fallback —
|
||||
* allows E2E tests and other custom SMC_DATA_DIR setups to
|
||||
* share the production credentials)
|
||||
*/
|
||||
export function getCredentialsPath(): string {
|
||||
const raw = process.env.SMC_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH;
|
||||
return expandHome(raw);
|
||||
// Explicit env override — use as-is
|
||||
if (process.env.SMC_CREDENTIALS_PATH) {
|
||||
return expandHome(process.env.SMC_CREDENTIALS_PATH);
|
||||
}
|
||||
|
||||
// Primary: current DATA_DIR
|
||||
if (existsSync(DEFAULT_CREDENTIALS_PATH)) {
|
||||
return DEFAULT_CREDENTIALS_PATH;
|
||||
}
|
||||
|
||||
// Fallback: default ~/.super-multica location when using a custom data dir
|
||||
if (DEFAULT_CREDENTIALS_PATH !== FALLBACK_CREDENTIALS_PATH && existsSync(FALLBACK_CREDENTIALS_PATH)) {
|
||||
return FALLBACK_CREDENTIALS_PATH;
|
||||
}
|
||||
|
||||
// Return primary path even if it doesn't exist (for error messages / creation)
|
||||
return DEFAULT_CREDENTIALS_PATH;
|
||||
}
|
||||
|
||||
export class CredentialManager {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@
|
|||
* - `tool_start` — Tool execution begins.
|
||||
* Fields: tool (name), args (first 500 chars of JSON)
|
||||
* - `tool_end` — Tool execution completes.
|
||||
* Fields: tool (name), duration_ms, is_error
|
||||
* Fields: tool (name), duration_ms, is_error, result_chars, result_summary?, error_type?
|
||||
* result_chars: total character count of result content (survives session compaction)
|
||||
* result_summary: short tool-specific summary (e.g. "10 results", "12.5KB", "finance/get_price_snapshot")
|
||||
* error_type: error category when tool returned an error (e.g. "fetch_failed", "ssrf_blocked")
|
||||
*
|
||||
* ### Context Management — Preflight (before LLM call)
|
||||
* - `preflight_compact_start` — Preflight compaction triggered.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,51 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean {
|
|||
return reason === "auth" || reason === "rate_limit" || reason === "billing" || reason === "timeout";
|
||||
}
|
||||
|
||||
// ── Run-log result extraction helpers ──────────────────────────────────────
|
||||
// Lightweight extractors for tool_end metadata. These mirror the patterns in
|
||||
// cli/output.ts but are kept separate to avoid CLI-specific dependencies.
|
||||
|
||||
function extractRunLogResultText(result: unknown): string | undefined {
|
||||
if (!result || typeof result !== "object") return undefined;
|
||||
const msg = result as { content?: Array<{ type: string; text?: string }> };
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const c of msg.content) {
|
||||
if (c.type === "text" && c.text) return c.text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractRunLogResultDetails(result: unknown): Record<string, unknown> | null {
|
||||
const text = extractRunLogResultText(result);
|
||||
if (text) {
|
||||
try { return JSON.parse(text) as Record<string, unknown>; } catch { /* non-JSON result */ }
|
||||
}
|
||||
const withDetails = result as { details?: unknown };
|
||||
if (withDetails?.details && typeof withDetails.details === "object") {
|
||||
return withDetails.details as Record<string, unknown>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatRunLogToolSummary(tool: string, details: Record<string, unknown> | null): string | undefined {
|
||||
if (!details) return undefined;
|
||||
if (details.error) return `error: ${details.code || details.message || details.error}`;
|
||||
switch (tool) {
|
||||
case "web_search": return `${details.count ?? 0} results`;
|
||||
case "web_fetch": {
|
||||
const parts: string[] = [];
|
||||
if (typeof details.length === "number") parts.push(`${(details.length as number / 1024).toFixed(1)}KB`);
|
||||
if (details.cached) parts.push("cached");
|
||||
return parts.join(", ") || undefined;
|
||||
}
|
||||
case "data": return `${details.domain}/${details.action}`;
|
||||
case "glob": return `${details.count ?? 0} files`;
|
||||
case "exec": return details.exitCode !== undefined ? `exit ${details.exitCode}` : undefined;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
private readonly agent: PiAgentCore;
|
||||
private output;
|
||||
|
|
@ -348,9 +393,12 @@ export class Agent {
|
|||
const profileToolsConfig = this.profile?.getToolsConfig();
|
||||
const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, options.tools);
|
||||
const profileDir = this.profile?.getProfileDir();
|
||||
// Use this.sessionId (which may be auto-generated) instead of options.sessionId
|
||||
// (which may be undefined). Without this, sessions_list and sessions_spawn
|
||||
// can't find sub-agent runs because they have no session context.
|
||||
this.toolsOptions = mergedToolsConfig
|
||||
? { ...options, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
|
||||
: { ...options, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider };
|
||||
? { ...options, sessionId: this.sessionId, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
|
||||
: { ...options, sessionId: this.sessionId, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider };
|
||||
|
||||
const tools = resolveTools(this.toolsOptions);
|
||||
if (this.debug) {
|
||||
|
|
@ -746,11 +794,24 @@ export class Agent {
|
|||
const startTime = this.toolStartTimes.get(toolName);
|
||||
const duration_ms = startTime ? Date.now() - startTime : undefined;
|
||||
this.toolStartTimes.delete(toolName);
|
||||
this.runLog.log("tool_end", {
|
||||
|
||||
// Extract result metadata for run-log persistence (survives session compaction)
|
||||
const result = (event as any).result;
|
||||
const resultText = extractRunLogResultText(result);
|
||||
const resultChars = resultText?.length ?? 0;
|
||||
const details = extractRunLogResultDetails(result);
|
||||
|
||||
const toolEndData: Record<string, unknown> = {
|
||||
tool: toolName,
|
||||
duration_ms,
|
||||
is_error: (event as any).isError ?? false,
|
||||
});
|
||||
result_chars: resultChars,
|
||||
result_summary: formatRunLogToolSummary(toolName, details),
|
||||
};
|
||||
if (details?.error) {
|
||||
toolEndData.error_type = details.code ? String(details.code) : String(details.error);
|
||||
}
|
||||
this.runLog.log("tool_end", toolEndData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
* Compatible with OpenClaw/AgentSkills specification
|
||||
*/
|
||||
|
||||
import { dirname } from "path";
|
||||
import type { Skill, SkillManagerOptions, SkillsConfig, SkillCommandSpec, SkillInvocationResult } from "./types.js";
|
||||
import { loadAllSkills, getProfileSkillsDir, initializeManagedSkills, getManagedSkillsDir } from "./loader.js";
|
||||
import {
|
||||
|
|
@ -332,6 +333,11 @@ export class SkillManager {
|
|||
parts.push(`## ${emoji} ${name} (${id})`);
|
||||
parts.push(`${desc}\n`);
|
||||
|
||||
// Include skill directory path so the agent can resolve relative paths
|
||||
// (e.g., scripts/recalc.py → /absolute/path/to/skill/scripts/recalc.py)
|
||||
const skillDir = dirname(skill.filePath);
|
||||
parts.push(`**Skill directory**: \`${skillDir}\`\n`);
|
||||
|
||||
// Include full instructions
|
||||
if (skill.instructions) {
|
||||
parts.push(skill.instructions);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ describe("sessions_list tool", () => {
|
|||
startedAt: now - 60000,
|
||||
endedAt: now - 30000,
|
||||
outcome: { status: "ok" },
|
||||
findings: "All tests passed successfully.",
|
||||
findingsCaptured: true,
|
||||
}),
|
||||
);
|
||||
seedSubagentRunForTests(
|
||||
|
|
@ -56,6 +58,8 @@ describe("sessions_list tool", () => {
|
|||
startedAt: now - 60000,
|
||||
endedAt: now,
|
||||
outcome: { status: "error", error: "timeout" },
|
||||
findings: "Lint check timed out.",
|
||||
findingsCaptured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -71,6 +75,13 @@ describe("sessions_list tool", () => {
|
|||
expect((text as { text: string }).text).toContain("Code Review");
|
||||
expect((text as { text: string }).text).toContain("Test Analysis");
|
||||
expect((text as { text: string }).text).toContain("Lint Check");
|
||||
// Verify full runId is shown for completed runs
|
||||
expect((text as { text: string }).text).toContain("id:run-aaa");
|
||||
expect((text as { text: string }).text).toContain("id:run-bbb");
|
||||
expect((text as { text: string }).text).toContain("id:run-ccc");
|
||||
// Verify findings are shown for completed runs
|
||||
expect((text as { text: string }).text).toContain("All tests passed successfully.");
|
||||
expect((text as { text: string }).text).toContain("Lint check timed out.");
|
||||
|
||||
expect(result.details!.runs).toHaveLength(3);
|
||||
expect(result.details!.runs[0]!.status).toBe("running");
|
||||
|
|
@ -141,6 +152,44 @@ describe("sessions_list tool", () => {
|
|||
expect(result.details).toEqual({ runs: [] });
|
||||
});
|
||||
|
||||
it("shows findings for grouped completed runs", async () => {
|
||||
const now = Date.now();
|
||||
const groupId = "group-001";
|
||||
seedSubagentRunForTests(
|
||||
makeRecord({
|
||||
runId: "run-g1",
|
||||
label: "Bull Case Research",
|
||||
startedAt: now - 60000,
|
||||
endedAt: now - 10000,
|
||||
outcome: { status: "ok" },
|
||||
findings: "AI infrastructure capex growing 40% YoY.",
|
||||
findingsCaptured: true,
|
||||
groupId,
|
||||
}),
|
||||
);
|
||||
seedSubagentRunForTests(
|
||||
makeRecord({
|
||||
runId: "run-g2",
|
||||
label: "Bear Case Research",
|
||||
startedAt: now - 60000,
|
||||
endedAt: now - 5000,
|
||||
outcome: { status: "ok" },
|
||||
findings: "Valuation risk: forward P/E above historical average.",
|
||||
findingsCaptured: true,
|
||||
groupId,
|
||||
}),
|
||||
);
|
||||
|
||||
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
||||
const result = await tool.execute("call-1", {});
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("id:run-g1");
|
||||
expect(text).toContain("id:run-g2");
|
||||
expect(text).toContain("AI infrastructure capex growing 40% YoY.");
|
||||
expect(text).toContain("Valuation risk: forward P/E above historical average.");
|
||||
});
|
||||
|
||||
it("shows findings status for running task", async () => {
|
||||
const now = Date.now();
|
||||
seedSubagentRunForTests(
|
||||
|
|
|
|||
|
|
@ -212,10 +212,13 @@ export function createSessionsListTool(
|
|||
const status = resolveStatus(r);
|
||||
if (status === "running") {
|
||||
const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned";
|
||||
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`);
|
||||
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`);
|
||||
} else {
|
||||
const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : "";
|
||||
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})`);
|
||||
const findings = r.findingsCaptured
|
||||
? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)")
|
||||
: "(findings not yet captured)";
|
||||
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,13 +230,13 @@ export function createSessionsListTool(
|
|||
const status = resolveStatus(r);
|
||||
if (status === "running") {
|
||||
const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned";
|
||||
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`);
|
||||
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`);
|
||||
} else {
|
||||
const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : "";
|
||||
const findings = r.findingsCaptured
|
||||
? (r.findings ? r.findings.slice(0, 200) + (r.findings.length > 200 ? "…" : "") : "(no output)")
|
||||
? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)")
|
||||
: "(findings not yet captured)";
|
||||
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})\n Findings: ${findings}`);
|
||||
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { createSessionsSpawnTool } from "./sessions-spawn.js";
|
||||
import { getSubagentGroup, resetSubagentRegistryForTests } from "../subagent/registry.js";
|
||||
|
||||
describe("sessions_spawn tool", () => {
|
||||
beforeEach(() => {
|
||||
resetSubagentRegistryForTests();
|
||||
});
|
||||
it("has correct name and description", () => {
|
||||
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "test-session" });
|
||||
expect(tool.name).toBe("sessions_spawn");
|
||||
|
|
@ -24,6 +28,23 @@ describe("sessions_spawn tool", () => {
|
|||
expect(firstContent.text).toContain("not allowed");
|
||||
});
|
||||
|
||||
it("auto-creates group when custom groupId is provided", async () => {
|
||||
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" });
|
||||
|
||||
// Should not error — the group is auto-created
|
||||
await tool.execute(
|
||||
"call-group",
|
||||
{ task: "research topic", label: "Research", groupId: "my-custom-group" } as any,
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
// Verify group was created in the registry
|
||||
const group = getSubagentGroup("my-custom-group");
|
||||
expect(group).toBeDefined();
|
||||
expect(group!.groupId).toBe("my-custom-group");
|
||||
expect(group!.label).toBe("Group: Research");
|
||||
});
|
||||
|
||||
it("fails gracefully when Hub is not initialized", async () => {
|
||||
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" });
|
||||
|
||||
|
|
|
|||
|
|
@ -126,19 +126,20 @@ export function createSessionsSpawnTool(
|
|||
const runId = uuidv7();
|
||||
const childSessionId = uuidv7();
|
||||
|
||||
// Validate groupId if provided
|
||||
// Auto-create group when groupId is provided but doesn't exist yet,
|
||||
// or when `next` is provided without a groupId.
|
||||
if (groupId) {
|
||||
const existingGroup = getSubagentGroup(groupId);
|
||||
if (!existingGroup) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: group not found: ${groupId}. Use the groupId returned by a previous sessions_spawn call.` }],
|
||||
details: { status: "error", error: `group not found: ${groupId}` },
|
||||
};
|
||||
// LLM provided a custom groupId — auto-create the group
|
||||
createSubagentGroup({
|
||||
groupId,
|
||||
requesterSessionId,
|
||||
label: label ? `Group: ${label}` : undefined,
|
||||
next,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-create group when `next` is provided without an existing groupId
|
||||
if (!groupId && next) {
|
||||
} else if (next) {
|
||||
groupId = uuidv7();
|
||||
createSubagentGroup({
|
||||
groupId,
|
||||
|
|
|
|||
|
|
@ -343,12 +343,14 @@ export function createWebFetchTool(): AgentTool<typeof WebFetchSchema, unknown>
|
|||
} catch (error) {
|
||||
if (error instanceof SsrfBlockedError) {
|
||||
return jsonResult({
|
||||
error: "ssrf_blocked",
|
||||
error: true,
|
||||
code: "ssrf_blocked",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
return jsonResult({
|
||||
error: "fetch_failed",
|
||||
error: true,
|
||||
code: "fetch_failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ export function createWebSearchTool(): AgentTool<typeof WebSearchSchema, unknown
|
|||
return jsonResult(result);
|
||||
} catch (error) {
|
||||
return jsonResult({
|
||||
error: "search_failed",
|
||||
error: true,
|
||||
code: "search_failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
|
@ -15944,9 +15944,9 @@ snapshots:
|
|||
'@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-expo: 1.0.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
globals: 16.5.0
|
||||
|
|
@ -15961,8 +15961,8 @@ snapshots:
|
|||
'@next/eslint-plugin-next': 16.1.6
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||
|
|
@ -15984,7 +15984,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -15995,18 +15995,18 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -16019,7 +16019,7 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -16030,7 +16030,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ This applies to ALL calculations - totals, percentages, ratios, differences, etc
|
|||
4. **Save**: Write to file
|
||||
5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the scripts/recalc.py script
|
||||
```bash
|
||||
python scripts/recalc.py output.xlsx
|
||||
python3 scripts/recalc.py output.xlsx
|
||||
```
|
||||
6. **Verify and fix any errors**:
|
||||
- The script returns JSON with error details
|
||||
|
|
@ -224,12 +224,12 @@ wb.save('modified.xlsx')
|
|||
Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `scripts/recalc.py` script to recalculate formulas:
|
||||
|
||||
```bash
|
||||
python scripts/recalc.py <excel_file> [timeout_seconds]
|
||||
python3 scripts/recalc.py <excel_file> [timeout_seconds]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
python scripts/recalc.py output.xlsx 30
|
||||
python3 scripts/recalc.py output.xlsx 30
|
||||
```
|
||||
|
||||
The script:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue