@finchagentic/mcp 4.0.0
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/LICENSE +21 -0
- package/README.md +345 -0
- package/dist/_http-cache.js +96 -0
- package/dist/agent-loop.js +231 -0
- package/dist/annotations.js +113 -0
- package/dist/cli.js +1195 -0
- package/dist/clink-input.js +15 -0
- package/dist/config.js +132 -0
- package/dist/convex.js +151 -0
- package/dist/dex-pair.js +54 -0
- package/dist/enrichment-router.js +315 -0
- package/dist/index.js +256 -0
- package/dist/llm.js +323 -0
- package/dist/local-memory.js +102 -0
- package/dist/local-vault.js +454 -0
- package/dist/output-schemas.js +551 -0
- package/dist/prompts.js +111 -0
- package/dist/public-url.js +107 -0
- package/dist/resources.js +116 -0
- package/dist/server.js +300 -0
- package/dist/signal-gate.js +57 -0
- package/dist/token-decimals.js +26 -0
- package/dist/token-gate.js +88 -0
- package/dist/tool-filter.js +44 -0
- package/dist/tools/_solidity-scan.js +313 -0
- package/dist/tools/agents.js +729 -0
- package/dist/tools/automation.js +314 -0
- package/dist/tools/base-mcp.js +478 -0
- package/dist/tools/base.js +269 -0
- package/dist/tools/chronicle.js +268 -0
- package/dist/tools/coder.js +94 -0
- package/dist/tools/deep-research.js +1416 -0
- package/dist/tools/defi.js +291 -0
- package/dist/tools/equity.js +364 -0
- package/dist/tools/events.js +182 -0
- package/dist/tools/framework.js +150 -0
- package/dist/tools/github.js +514 -0
- package/dist/tools/insider.js +264 -0
- package/dist/tools/insight.js +634 -0
- package/dist/tools/market.js +555 -0
- package/dist/tools/memory.js +1046 -0
- package/dist/tools/miroshark.js +343 -0
- package/dist/tools/monitor.js +319 -0
- package/dist/tools/os.js +226 -0
- package/dist/tools/packets.js +296 -0
- package/dist/tools/research-chain.js +226 -0
- package/dist/tools/research-compare.js +280 -0
- package/dist/tools/research.js +188 -0
- package/dist/tools/rh-bridge.js +148 -0
- package/dist/tools/rh-mcp.js +1411 -0
- package/dist/tools/rh-orders.js +471 -0
- package/dist/tools/scanner.js +534 -0
- package/dist/tools/vault.js +764 -0
- package/dist/tools/wallet.js +200 -0
- package/dist/types.js +2 -0
- package/dist/wallet.js +184 -0
- package/package.json +87 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_TOOLS = void 0;
|
|
4
|
+
exports.buildAgentList = buildAgentList;
|
|
5
|
+
exports.buildAgentLedger = buildAgentLedger;
|
|
6
|
+
exports.buildAgentRuns = buildAgentRuns;
|
|
7
|
+
exports.handleAgentTool = handleAgentTool;
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const ethers_1 = require("ethers");
|
|
10
|
+
const convex_js_1 = require("../convex.js");
|
|
11
|
+
const llm_js_1 = require("../llm.js");
|
|
12
|
+
// ─── Agent Learning Memory (v3.25) ──────────────────────────────────────────
|
|
13
|
+
// After every agent_update, an LLM reviews the new progress in context of the
|
|
14
|
+
// agent's goal + prior learnings to extract a single repeatable insight. The
|
|
15
|
+
// insight is appended to the agent's `learnings[]` array (capped to last 30
|
|
16
|
+
// to keep the state file from bloating). On agent_recall and at the start of
|
|
17
|
+
// any agent run, learnings are surfaced so subsequent work compounds.
|
|
18
|
+
//
|
|
19
|
+
// Failure mode: extraction is best-effort. If the LLM is unavailable, slow,
|
|
20
|
+
// or returns NONE, the update still succeeds - we just don't append. Learning
|
|
21
|
+
// is a quality bonus, not a hard requirement of the update path.
|
|
22
|
+
const MAX_LEARNINGS = 30;
|
|
23
|
+
const LEARNING_SYSTEM_PROMPT = [
|
|
24
|
+
"You are an expert at extracting one reusable insight from an agent's progress update.",
|
|
25
|
+
"",
|
|
26
|
+
"You will see: the agent's goal, the agent's recent updates (chronological), and the latest update.",
|
|
27
|
+
"",
|
|
28
|
+
"TASK: Decide if the latest update reveals a NEW repeatable pattern, heuristic, or framework that would",
|
|
29
|
+
"improve this agent's future runs. Be strict - most updates are routine and do NOT contain a new insight.",
|
|
30
|
+
"",
|
|
31
|
+
"Output rules:",
|
|
32
|
+
"- If a genuine new insight emerged, output ONE sentence under 180 chars starting with an imperative verb.",
|
|
33
|
+
" Examples: \"Prefer Morpho vaults over Aave when USDC supply exceeds $10M.\"",
|
|
34
|
+
" \"Always check on-chain holder count before claiming a token has organic demand.\"",
|
|
35
|
+
" \"Skip schedule_research for time-sensitive queries - use deep_research with freshMode.\"",
|
|
36
|
+
"- If the update is routine (status report, progress without insight, repeat of prior learning),",
|
|
37
|
+
" output exactly: NONE",
|
|
38
|
+
"- Do not output explanations, prefixes, or quotation marks. Just the sentence or NONE.",
|
|
39
|
+
].join("\n");
|
|
40
|
+
async function extractLearning(goal, priorUpdates, priorLearnings, latest) {
|
|
41
|
+
// Fast skip when no LLM key is configured. Without a key, callLLM falls
|
|
42
|
+
// through to the finch-proxy path which may also be unconfigured for
|
|
43
|
+
// this user - every agent_update would otherwise pay an 8s timeout for
|
|
44
|
+
// nothing. Cheap, correct, and keeps update latency low.
|
|
45
|
+
const hasLLM = !!(process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GROK_API_KEY);
|
|
46
|
+
if (!hasLLM && !process.env.FINCH_SESSION_TOKEN) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Build the user prompt with all the context the model needs.
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push(`Agent goal: ${goal}`);
|
|
52
|
+
if (priorLearnings.length) {
|
|
53
|
+
lines.push(``);
|
|
54
|
+
lines.push(`Existing learnings (do not repeat any of these):`);
|
|
55
|
+
priorLearnings.slice(-10).forEach((l, i) => lines.push(` ${i + 1}. ${l}`));
|
|
56
|
+
}
|
|
57
|
+
if (priorUpdates.length) {
|
|
58
|
+
lines.push(``);
|
|
59
|
+
lines.push(`Recent updates (oldest first):`);
|
|
60
|
+
priorUpdates.slice(-5).forEach((u, i) => {
|
|
61
|
+
const finding = u.findings ? ` | findings: ${u.findings}` : "";
|
|
62
|
+
lines.push(` ${i + 1}. [${u.status ?? "active"}] ${u.progress}${finding}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
lines.push(``);
|
|
66
|
+
lines.push(`Latest update:`);
|
|
67
|
+
lines.push(` [${latest.status ?? "active"}] ${latest.progress}${latest.findings ? ` | findings: ${latest.findings}` : ""}`);
|
|
68
|
+
lines.push(``);
|
|
69
|
+
lines.push(`What new repeatable insight (if any) emerged from this latest update?`);
|
|
70
|
+
try {
|
|
71
|
+
// Short max_tokens - the answer should be one sentence. 8s timeout keeps
|
|
72
|
+
// agent_update responsive even when the LLM is slow; mutex still serializes
|
|
73
|
+
// updates so we don't pile up.
|
|
74
|
+
const out = await (0, llm_js_1.callLLM)(LEARNING_SYSTEM_PROMPT, lines.join("\n"), 120, [], 8000);
|
|
75
|
+
const trimmed = out.trim().replace(/^["']|["']$/g, "");
|
|
76
|
+
if (!trimmed || trimmed.toUpperCase() === "NONE")
|
|
77
|
+
return null;
|
|
78
|
+
// Guard against the model accidentally repeating a recent learning.
|
|
79
|
+
const lowered = trimmed.toLowerCase();
|
|
80
|
+
if (priorLearnings.some((p) => p.toLowerCase() === lowered))
|
|
81
|
+
return null;
|
|
82
|
+
// Hard cap on length to keep state file bounded.
|
|
83
|
+
return trimmed.length > 200 ? trimmed.slice(0, 200) : trimmed;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Per-agent mutex for the read-modify-write cycle in agent_update / agent_spawn.
|
|
90
|
+
// Without this, two parallel calls on the same agent would both read the same
|
|
91
|
+
// state, both append their own update, and the second save would silently lose
|
|
92
|
+
// the first update. The mutex chains all writes for a given agent name through
|
|
93
|
+
// a single Promise so they execute serially.
|
|
94
|
+
const agentLocks = new Map();
|
|
95
|
+
function withAgentLock(agentName, fn) {
|
|
96
|
+
const prev = agentLocks.get(agentName) ?? Promise.resolve();
|
|
97
|
+
const next = prev.then(() => fn(), () => fn());
|
|
98
|
+
agentLocks.set(agentName, next);
|
|
99
|
+
// Clean up the map entry once this op completes - otherwise long-lived
|
|
100
|
+
// server processes accumulate dead entries.
|
|
101
|
+
next.finally(() => {
|
|
102
|
+
if (agentLocks.get(agentName) === next)
|
|
103
|
+
agentLocks.delete(agentName);
|
|
104
|
+
});
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
exports.AGENT_TOOLS = [
|
|
108
|
+
{
|
|
109
|
+
name: "list_agents",
|
|
110
|
+
description: "List all available specialist agents you can hire - built-in experts (analyst, risk-manager, researcher, executor, scout) plus any community-published agents.",
|
|
111
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "hire_agent",
|
|
115
|
+
description: "Load a specialist agent's expertise and apply it to a task YOURSELF — the tool returns the agent's " +
|
|
116
|
+
"full persona (its framework, thresholds and house rules) scoped to your task, and you answer in that " +
|
|
117
|
+
"voice. Built-in: analyst, risk-manager, researcher, executor, scout; use list_agents for the rest. " +
|
|
118
|
+
"No API key needed. Pass `execute: true` only when you need the agent's own model to answer instead " +
|
|
119
|
+
"(costs a server-side LLM call and you cannot steer the reasoning).",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
agentId: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Agent ID from list_agents. Built-in: analyst, risk-manager, researcher, executor, scout. Or a custom agent ID.",
|
|
126
|
+
},
|
|
127
|
+
task: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "The task or question for the agent. Be specific - better input = better output.",
|
|
130
|
+
},
|
|
131
|
+
execute: {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
description: "Run the agent server-side instead of returning its persona for you to apply. Default false. Requires a server LLM key.",
|
|
134
|
+
},
|
|
135
|
+
maxTokens: {
|
|
136
|
+
type: "number",
|
|
137
|
+
description: "execute:true only. Max response tokens (default 800, max 1200).",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ["agentId", "task"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "agent_spawn",
|
|
145
|
+
description: "Create a persistent NAMED agent with a goal - survives across sessions, state saved to vault under `agent/<name>` key. " +
|
|
146
|
+
"Use this when a task is ONGOING and will span multiple sessions: research you'll return to, a project you're tracking, " +
|
|
147
|
+
"a workflow you're iterating on. Track progress with agent_update, resume with agent_recall, audit with agent_ledger. " +
|
|
148
|
+
"Do NOT spawn an agent for one-shot tasks (single research query, single trade) - just call the relevant tool directly. " +
|
|
149
|
+
"Do NOT use this for ephemeral background data - use memory_add for that instead.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
name: { type: "string", description: "Unique agent name (e.g. 'market-researcher', 'onboarding-helper')" },
|
|
154
|
+
goal: { type: "string", description: "What this agent is trying to accomplish" },
|
|
155
|
+
context: { type: "string", description: "Optional starting context, data, or notes for the agent" },
|
|
156
|
+
},
|
|
157
|
+
required: ["name", "goal"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "agent_recall",
|
|
162
|
+
description: "Recall a persistent agent by name - loads its goal, current progress, findings, full history, and accumulated learnings (patterns the agent extracted from past runs). " +
|
|
163
|
+
"Use this to resume a long-running task, check what an agent last did, or hand context to a fresh LLM session. " +
|
|
164
|
+
"Learnings compound over time - the more an agent runs, the smarter recall becomes.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
name: { type: "string", description: "Agent name as used in agent_spawn" },
|
|
169
|
+
},
|
|
170
|
+
required: ["name"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "agent_update",
|
|
175
|
+
description: "Update a persistent agent's progress and findings. Creates a new vault version automatically - full history preserved. " +
|
|
176
|
+
"After each update an LLM extracts a single repeatable insight (if any) and appends it to the agent's accumulated learnings - the agent gets smarter every run. " +
|
|
177
|
+
"Status options: active | blocked | complete.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
name: { type: "string", description: "Agent name" },
|
|
182
|
+
progress: { type: "string", description: "What was accomplished in this update" },
|
|
183
|
+
findings: { type: "string", description: "Key findings, data, or outputs from this step" },
|
|
184
|
+
status: { type: "string", enum: ["active", "blocked", "complete"], description: "Current agent status (default: active)" },
|
|
185
|
+
nextStep: { type: "string", description: "What should happen next (optional - helps on recall)" },
|
|
186
|
+
},
|
|
187
|
+
required: ["name", "progress"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "agent_identity",
|
|
192
|
+
description: "Get or create a persistent on-chain identity (wallet address) for a named agent. " +
|
|
193
|
+
"Every agent gets a unique Base address that acts as its digital identity - visible in the app, " +
|
|
194
|
+
"usable for receiving payments, and permanently tied to that agent name. " +
|
|
195
|
+
"Call once per agent; calling again returns the same address.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
agentId: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Agent ID or name (e.g. 'market-researcher', 'analyst')",
|
|
202
|
+
},
|
|
203
|
+
agentName: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Human-readable agent name for display (optional)",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ["agentId"],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "agent_ledger",
|
|
213
|
+
description: "View the full activity ledger for a persistent agent - every update, status change, and finding logged in order. " +
|
|
214
|
+
"Each entry is a vault version created by agent_update. Use this to audit what an agent has done, " +
|
|
215
|
+
"trace its reasoning, or review progress since spawn.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
name: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Agent name as used in agent_spawn",
|
|
222
|
+
},
|
|
223
|
+
limit: {
|
|
224
|
+
type: "number",
|
|
225
|
+
description: "Max entries to return (default 20, max 50)",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
required: ["name"],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "agent_schedule",
|
|
233
|
+
description: "Attach an autonomous schedule to an existing agent. The agent wakes up on cron cadence, executes a workflow, " +
|
|
234
|
+
"saves the result to vault under `agent/<name>/runs/<date>`, and optionally pings you on Telegram. " +
|
|
235
|
+
"Workflows: " +
|
|
236
|
+
"deep_research (LLM brief on agent's goal) · " +
|
|
237
|
+
"packet (run a named packet) · " +
|
|
238
|
+
"llm (agentic loop with restricted tools) · " +
|
|
239
|
+
"reflection (walk recent vault activity → update profile/state for persistent context). " +
|
|
240
|
+
"Cron shorthand: hourly | daily | weekly | every-2-minutes | every-5-minutes (last two for testing).",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
name: { type: "string", description: "Agent name (must already exist via agent_spawn)" },
|
|
245
|
+
cron: { type: "string", description: "Schedule: hourly | daily | weekly | every-2-minutes | every-5-minutes" },
|
|
246
|
+
workflow: { type: "string", enum: ["deep_research", "packet", "llm", "reflection"], description: "Workflow type" },
|
|
247
|
+
topic: { type: "string", description: "[deep_research] Research topic (defaults to agent's goal)" },
|
|
248
|
+
packetName: { type: "string", description: "[packet] Name of packet to run (must exist via packet_create)" },
|
|
249
|
+
prompt: { type: "string", description: "[llm] Instructions for the autonomous agent loop" },
|
|
250
|
+
allowedTools: { type: "array", items: { type: "string" }, description: "[llm] Tool whitelist - subset of: vault_save, vault_search, get_market_data, web_search" },
|
|
251
|
+
windowDays: { type: "number", description: "[reflection] Days of vault activity to walk (default 7)" },
|
|
252
|
+
maxRunsPerDay: { type: "number", description: "Cost ceiling - default 4 (daily), 24 (hourly), 1 (weekly)" },
|
|
253
|
+
},
|
|
254
|
+
required: ["name", "cron", "workflow"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "agent_unschedule",
|
|
259
|
+
description: "Remove the autonomous schedule from an agent. The agent itself remains in vault - only its scheduled execution is cleared.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: { name: { type: "string", description: "Agent name" } },
|
|
263
|
+
required: ["name"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "agent_pause",
|
|
268
|
+
description: "Pause an agent's autonomous schedule without deleting it. Use agent_resume to re-enable.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: { name: { type: "string", description: "Agent name" } },
|
|
272
|
+
required: ["name"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "agent_resume",
|
|
277
|
+
description: "Re-enable a paused agent's schedule. Resets the consecutive-failure counter.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: { name: { type: "string", description: "Agent name" } },
|
|
281
|
+
required: ["name"],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "agent_runs",
|
|
286
|
+
description: "View recent autonomous run history for a scheduled agent - when it ran, success/failure status, " +
|
|
287
|
+
"vault key where the output was saved, and duration. Use this to verify the autonomous loop is healthy.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: "string", description: "Agent name" },
|
|
292
|
+
limit: { type: "number", description: "Max runs to return (default 10, max 50)" },
|
|
293
|
+
},
|
|
294
|
+
required: ["name"],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
const HireAgentSchema = zod_1.z.object({
|
|
299
|
+
agentId: zod_1.z.string().min(1),
|
|
300
|
+
task: zod_1.z.string().min(1),
|
|
301
|
+
execute: zod_1.z.boolean().optional(),
|
|
302
|
+
maxTokens: zod_1.z.number().int().min(100).max(1200).optional(),
|
|
303
|
+
});
|
|
304
|
+
const SpawnAgentSchema = zod_1.z.object({
|
|
305
|
+
name: zod_1.z.string().min(1).max(60).regex(/^[a-z0-9-]+$/, "name must be lowercase alphanumeric with hyphens"),
|
|
306
|
+
goal: zod_1.z.string().min(1),
|
|
307
|
+
context: zod_1.z.string().optional(),
|
|
308
|
+
});
|
|
309
|
+
const RecallAgentSchema = zod_1.z.object({ name: zod_1.z.string().min(1) });
|
|
310
|
+
const UpdateAgentSchema = zod_1.z.object({
|
|
311
|
+
name: zod_1.z.string().min(1),
|
|
312
|
+
progress: zod_1.z.string().min(1),
|
|
313
|
+
findings: zod_1.z.string().optional(),
|
|
314
|
+
status: zod_1.z.enum(["active", "blocked", "complete"]).optional(),
|
|
315
|
+
nextStep: zod_1.z.string().optional(),
|
|
316
|
+
});
|
|
317
|
+
// ── Structured output builders (schemas in output-schemas.ts) ───────────────
|
|
318
|
+
function buildAgentList(agents) {
|
|
319
|
+
return {
|
|
320
|
+
count: agents.length,
|
|
321
|
+
agents: agents.map((a) => ({
|
|
322
|
+
id: a.id,
|
|
323
|
+
name: a.name,
|
|
324
|
+
description: a.description ?? null,
|
|
325
|
+
category: a.category ?? null,
|
|
326
|
+
pricingType: a.pricingType ?? null,
|
|
327
|
+
runs: a.runs ?? null,
|
|
328
|
+
})),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function buildAgentLedger(name, versions) {
|
|
332
|
+
return {
|
|
333
|
+
name,
|
|
334
|
+
count: versions.length,
|
|
335
|
+
versions: versions.map((v) => ({
|
|
336
|
+
version: v.version,
|
|
337
|
+
commitMsg: v.commitMsg ?? null,
|
|
338
|
+
createdAt: v.createdAt ?? null,
|
|
339
|
+
})),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function buildAgentRuns(name, runs) {
|
|
343
|
+
return {
|
|
344
|
+
name,
|
|
345
|
+
count: runs.length,
|
|
346
|
+
runs: runs.map((r) => ({
|
|
347
|
+
startedAt: r.startedAt ?? null,
|
|
348
|
+
endedAt: r.endedAt ?? null,
|
|
349
|
+
status: r.status ?? null,
|
|
350
|
+
workflow: r.workflow ?? null,
|
|
351
|
+
durationMs: r.durationMs ?? null,
|
|
352
|
+
toolCallCount: r.toolCallCount ?? null,
|
|
353
|
+
resultSummary: r.resultSummary ?? null,
|
|
354
|
+
errorMsg: r.errorMsg ?? null,
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function handleAgentTool(name, args) {
|
|
359
|
+
if (name === "list_agents") {
|
|
360
|
+
const data = await (0, convex_js_1.callConvex)("/agents/list", "GET", undefined, "list_agents");
|
|
361
|
+
const agents = data.agents ?? [];
|
|
362
|
+
const lines = agents.map((a) => {
|
|
363
|
+
const badge = a.pricingType === "free" ? "free" : "token-based";
|
|
364
|
+
const runs = a.runs != null ? ` · ${a.runs} runs` : "";
|
|
365
|
+
return `**${a.name}** (\`${a.id}\`) [${badge}${runs}]\n ${a.description}`;
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
content: [{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: `## Available Agents (${agents.length})\n\n${lines.join("\n\n")}`,
|
|
371
|
+
}],
|
|
372
|
+
structuredContent: buildAgentList(agents),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (name === "hire_agent") {
|
|
376
|
+
const parsed = HireAgentSchema.safeParse(args);
|
|
377
|
+
if (!parsed.success) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }],
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const { agentId, task, maxTokens, execute = false } = parsed.data;
|
|
384
|
+
const data = await (0, convex_js_1.callConvex)("/agents/hire", "POST", { agentId, task, maxTokens, mode: execute ? "run" : "brief" }, "hire_agent");
|
|
385
|
+
if (data.error) {
|
|
386
|
+
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
387
|
+
}
|
|
388
|
+
if (execute) {
|
|
389
|
+
const footer = data.tokensUsed ? `\n\n*Tokens used: ${data.tokensUsed}*` : "";
|
|
390
|
+
return {
|
|
391
|
+
content: [{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `## ${data.agent ?? agentId} - Response\n\n${data.result}${footer}`,
|
|
394
|
+
}],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// The registry lookup is the tool's real work - it holds each agent's
|
|
398
|
+
// persona, thresholds and hard rules. Applying them is model work, and the
|
|
399
|
+
// caller's model is the better one *and* the one the user can redirect.
|
|
400
|
+
if (!data.systemPrompt) {
|
|
401
|
+
return {
|
|
402
|
+
content: [{
|
|
403
|
+
type: "text",
|
|
404
|
+
text: `Agent \`${agentId}\` returned no persona. The backend may predate brief mode — retry with \`execute: true\`.`,
|
|
405
|
+
}],
|
|
406
|
+
isError: true,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
content: [{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: [
|
|
413
|
+
`## Adopt: ${data.agent ?? agentId}`,
|
|
414
|
+
``,
|
|
415
|
+
`You are now this specialist. Answer the task below **in this persona**, applying its framework, ` +
|
|
416
|
+
`thresholds and hard rules exactly as written — including the ones that tell you to refuse, ` +
|
|
417
|
+
`skip or downgrade a setup. Those gates are the reason the persona exists; do not soften them ` +
|
|
418
|
+
`to give a more agreeable answer.`,
|
|
419
|
+
``,
|
|
420
|
+
`---`,
|
|
421
|
+
``,
|
|
422
|
+
data.systemPrompt,
|
|
423
|
+
``,
|
|
424
|
+
`---`,
|
|
425
|
+
``,
|
|
426
|
+
`### Task`,
|
|
427
|
+
``,
|
|
428
|
+
task,
|
|
429
|
+
].join("\n"),
|
|
430
|
+
}],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (name === "agent_spawn") {
|
|
434
|
+
const parsed = SpawnAgentSchema.safeParse(args);
|
|
435
|
+
if (!parsed.success)
|
|
436
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
437
|
+
const { name: agentName, goal, context } = parsed.data;
|
|
438
|
+
const content = JSON.stringify({
|
|
439
|
+
goal,
|
|
440
|
+
status: "active",
|
|
441
|
+
spawnedAt: new Date().toISOString(),
|
|
442
|
+
context: context ?? null,
|
|
443
|
+
updates: [],
|
|
444
|
+
}, null, 2);
|
|
445
|
+
const data = await (0, convex_js_1.callConvex)("/vault/save", "POST", {
|
|
446
|
+
type: "memory",
|
|
447
|
+
key: `agent/${agentName}`,
|
|
448
|
+
title: `Agent: ${agentName}`,
|
|
449
|
+
content,
|
|
450
|
+
contentType: "json",
|
|
451
|
+
agentId: agentName,
|
|
452
|
+
tags: ["persistent-agent"],
|
|
453
|
+
commitMsg: "spawned",
|
|
454
|
+
}, "vault_save");
|
|
455
|
+
if (data.error)
|
|
456
|
+
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
457
|
+
return {
|
|
458
|
+
content: [{ type: "text", text: `🤖 Agent **${agentName}** spawned.\n\n**Goal:** ${goal}\n\nRecall with \`agent_recall\` · Update progress with \`agent_update\`` }],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (name === "agent_recall") {
|
|
462
|
+
const parsed = RecallAgentSchema.safeParse(args);
|
|
463
|
+
if (!parsed.success)
|
|
464
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
465
|
+
const data = await (0, convex_js_1.callConvex)(`/vault/entry?key=agent/${parsed.data.name}`, "GET", undefined, "vault_read");
|
|
466
|
+
if (data.error || !data.content) {
|
|
467
|
+
return { content: [{ type: "text", text: `Agent \`${parsed.data.name}\` not found. Spawn it first with \`agent_spawn\`.` }], isError: true };
|
|
468
|
+
}
|
|
469
|
+
let state = {};
|
|
470
|
+
try {
|
|
471
|
+
state = JSON.parse(data.content);
|
|
472
|
+
}
|
|
473
|
+
catch { /* non-JSON content */ }
|
|
474
|
+
const updates = (state.updates ?? []).slice(-3).map((u, i) => ` ${i + 1}. [${u.status ?? "active"}] ${u.progress}${u.nextStep ? ` → next: ${u.nextStep}` : ""}`);
|
|
475
|
+
// Learnings - the agent's accumulated expertise across all prior updates.
|
|
476
|
+
// Surfaced prominently so the caller (or downstream LLM) can apply them.
|
|
477
|
+
const learningEntries = Array.isArray(state.learnings)
|
|
478
|
+
? state.learnings.map((l) => (typeof l === "string" ? l : l?.learned)).filter(Boolean)
|
|
479
|
+
: [];
|
|
480
|
+
const lines = [
|
|
481
|
+
`## Agent: ${parsed.data.name}`,
|
|
482
|
+
`**Goal:** ${state.goal ?? "-"}`,
|
|
483
|
+
`**Status:** ${state.status ?? "active"}`,
|
|
484
|
+
`**Version:** ${data.version ?? 1} · Updated: ${data.updatedAt ? new Date(data.updatedAt).toUTCString() : "-"}`,
|
|
485
|
+
];
|
|
486
|
+
if (state.context)
|
|
487
|
+
lines.push(`**Context:** ${state.context}`);
|
|
488
|
+
if (learningEntries.length) {
|
|
489
|
+
lines.push(`\n**🧠 Learned patterns (${learningEntries.length}):**`);
|
|
490
|
+
// Show the most recent 8 - these are the most refined and relevant.
|
|
491
|
+
learningEntries.slice(-8).forEach((l, i) => lines.push(` ${i + 1}. ${l}`));
|
|
492
|
+
}
|
|
493
|
+
if (updates.length)
|
|
494
|
+
lines.push(`\n**Recent updates:**\n${updates.join("\n")}`);
|
|
495
|
+
if (state.nextStep)
|
|
496
|
+
lines.push(`\n**Next step:** ${state.nextStep}`);
|
|
497
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
498
|
+
}
|
|
499
|
+
if (name === "agent_update") {
|
|
500
|
+
const parsed = UpdateAgentSchema.safeParse(args);
|
|
501
|
+
if (!parsed.success)
|
|
502
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
503
|
+
const { name: agentName, progress, findings, status = "active", nextStep } = parsed.data;
|
|
504
|
+
return withAgentLock(agentName, async () => {
|
|
505
|
+
// Load current state - inside the lock so two parallel updates can't
|
|
506
|
+
// both read v=N and both write v=N+1 (the second silently loses).
|
|
507
|
+
const current = await (0, convex_js_1.callConvex)(`/vault/entry?key=agent/${agentName}`, "GET", undefined, "vault_read");
|
|
508
|
+
if (current.error || !current.content) {
|
|
509
|
+
return { content: [{ type: "text", text: `Agent \`${agentName}\` not found. Spawn it first.` }], isError: true };
|
|
510
|
+
}
|
|
511
|
+
let state = {};
|
|
512
|
+
try {
|
|
513
|
+
state = JSON.parse(current.content);
|
|
514
|
+
}
|
|
515
|
+
catch { /* start fresh */ }
|
|
516
|
+
const update = { progress, status, timestamp: new Date().toISOString() };
|
|
517
|
+
if (findings)
|
|
518
|
+
update.findings = findings;
|
|
519
|
+
if (nextStep)
|
|
520
|
+
update.nextStep = nextStep;
|
|
521
|
+
state.status = status;
|
|
522
|
+
state.nextStep = nextStep ?? state.nextStep;
|
|
523
|
+
state.updates = [...(state.updates ?? []), update].slice(-20);
|
|
524
|
+
// ─── Learning extraction (v3.25) ────────────────────────────────────
|
|
525
|
+
// Ask an LLM whether this update revealed a new repeatable insight.
|
|
526
|
+
// Best-effort: if it returns NONE or fails, we save without appending.
|
|
527
|
+
// The call sits inside the mutex so concurrent updates don't race on
|
|
528
|
+
// the learnings array - same protection that already covers updates[].
|
|
529
|
+
const priorLearnings = Array.isArray(state.learnings)
|
|
530
|
+
? state.learnings.map((l) => (typeof l === "string" ? l : l?.learned)).filter(Boolean)
|
|
531
|
+
: [];
|
|
532
|
+
const newLearning = await extractLearning(state.goal ?? "", (state.updates ?? []).slice(0, -1), priorLearnings, update);
|
|
533
|
+
if (newLearning) {
|
|
534
|
+
const entry = { learned: newLearning, ts: Date.now(), fromUpdate: state.updates.length - 1 };
|
|
535
|
+
state.learnings = [...(state.learnings ?? []), entry].slice(-MAX_LEARNINGS);
|
|
536
|
+
}
|
|
537
|
+
const data = await (0, convex_js_1.callConvex)("/vault/save", "POST", {
|
|
538
|
+
type: "memory",
|
|
539
|
+
key: `agent/${agentName}`,
|
|
540
|
+
title: `Agent: ${agentName}`,
|
|
541
|
+
content: JSON.stringify(state, null, 2),
|
|
542
|
+
contentType: "json",
|
|
543
|
+
agentId: agentName,
|
|
544
|
+
commitMsg: `[${status}] ${progress.slice(0, 60)}${newLearning ? " · +learning" : ""}`,
|
|
545
|
+
}, "vault_save");
|
|
546
|
+
if (data.error)
|
|
547
|
+
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
548
|
+
const statusEmoji = status === "complete" ? "✅" : status === "blocked" ? "🚫" : "🔄";
|
|
549
|
+
const learningLine = newLearning
|
|
550
|
+
? `\n\n🧠 **Learned:** ${newLearning}`
|
|
551
|
+
: "";
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: "text", text: `${statusEmoji} Agent **${agentName}** updated (v${data.version}).\n\n**Progress:** ${progress}${findings ? `\n**Findings:** ${findings}` : ""}${nextStep ? `\n**Next:** ${nextStep}` : ""}${learningLine}` }],
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
if (name === "agent_ledger") {
|
|
558
|
+
const { name: agentName, limit = 20 } = args;
|
|
559
|
+
if (!agentName)
|
|
560
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
561
|
+
const cap = Math.min(Math.max(1, limit), 50);
|
|
562
|
+
const data = await (0, convex_js_1.callConvex)(`/vault/history?key=agent/${encodeURIComponent(agentName)}&limit=${cap}`, "GET", undefined, "vault_history");
|
|
563
|
+
if (data.error)
|
|
564
|
+
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
565
|
+
const versions = data.history ?? [];
|
|
566
|
+
if (!versions.length) {
|
|
567
|
+
return { content: [{ type: "text", text: `No ledger entries found for agent \`${agentName}\`. Spawn it first with \`agent_spawn\`.` }], structuredContent: buildAgentLedger(agentName, []) };
|
|
568
|
+
}
|
|
569
|
+
const rows = versions.map((v) => {
|
|
570
|
+
const ts = v.createdAt ? new Date(v.createdAt).toUTCString() : "-";
|
|
571
|
+
const msg = v.commitMsg ?? "(no message)";
|
|
572
|
+
return ` v${v.version} ${ts}\n ${msg}`;
|
|
573
|
+
});
|
|
574
|
+
return {
|
|
575
|
+
content: [{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: `## Agent Ledger: ${agentName} (${versions.length} entries)\n\n${rows.join("\n\n")}`,
|
|
578
|
+
}],
|
|
579
|
+
structuredContent: buildAgentLedger(agentName, versions),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (name === "agent_identity") {
|
|
583
|
+
const { agentId, agentName } = args;
|
|
584
|
+
// Check if identity already exists
|
|
585
|
+
const existing = await (0, convex_js_1.callConvex)(`/agents/identity?agentId=${encodeURIComponent(agentId)}`, "GET", undefined, "agent_identity");
|
|
586
|
+
if (existing?.identity) {
|
|
587
|
+
const id = existing.identity;
|
|
588
|
+
return {
|
|
589
|
+
content: [{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: [
|
|
592
|
+
`🤖 **Agent Identity: \`${agentId}\`**`,
|
|
593
|
+
``,
|
|
594
|
+
`**Address:** \`${id.walletAddress}\``,
|
|
595
|
+
`**Network:** Base mainnet`,
|
|
596
|
+
``,
|
|
597
|
+
`This address is permanent and tied to this agent.`,
|
|
598
|
+
`Share it to receive funds · Track spending · Verify agent actions`,
|
|
599
|
+
].join("\n"),
|
|
600
|
+
}],
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
// Generate new wallet address for the agent
|
|
604
|
+
const wallet = ethers_1.ethers.Wallet.createRandom();
|
|
605
|
+
await (0, convex_js_1.callConvex)("/agents/identity", "POST", {
|
|
606
|
+
agentId,
|
|
607
|
+
walletAddress: wallet.address,
|
|
608
|
+
agentName: agentName ?? agentId,
|
|
609
|
+
}, "agent_identity");
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: [
|
|
614
|
+
`🤖 **Agent Identity Created: \`${agentId}\`**`,
|
|
615
|
+
``,
|
|
616
|
+
`**Address:** \`${wallet.address}\``,
|
|
617
|
+
`**Network:** Base mainnet`,
|
|
618
|
+
``,
|
|
619
|
+
`This identity is now permanently linked to \`${agentId}\`.`,
|
|
620
|
+
`Visible in the Agents page of your Finch app.`,
|
|
621
|
+
].join("\n"),
|
|
622
|
+
}],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// ─── Autonomous schedule tools ──────────────────────────────────────────────
|
|
626
|
+
if (name === "agent_schedule") {
|
|
627
|
+
const a = args;
|
|
628
|
+
if (!a.name || !a.cron || !a.workflow) {
|
|
629
|
+
return { content: [{ type: "text", text: "name, cron, workflow required" }], isError: true };
|
|
630
|
+
}
|
|
631
|
+
const agentKey = `agent/${a.name}`;
|
|
632
|
+
const workflowConfig = {};
|
|
633
|
+
if (a.workflow === "deep_research" && a.topic)
|
|
634
|
+
workflowConfig.topic = a.topic;
|
|
635
|
+
if (a.workflow === "packet")
|
|
636
|
+
workflowConfig.packetName = a.packetName;
|
|
637
|
+
if (a.workflow === "llm") {
|
|
638
|
+
workflowConfig.prompt = a.prompt;
|
|
639
|
+
workflowConfig.allowedTools = a.allowedTools ?? ["vault_save", "vault_search"];
|
|
640
|
+
}
|
|
641
|
+
if (a.workflow === "reflection") {
|
|
642
|
+
workflowConfig.windowDays = typeof a.windowDays === "number" ? a.windowDays : 7;
|
|
643
|
+
}
|
|
644
|
+
const result = await (0, convex_js_1.callConvex)("/agents/schedule", "POST", {
|
|
645
|
+
agentKey, cron: a.cron, workflow: a.workflow, workflowConfig,
|
|
646
|
+
maxRunsPerDay: a.maxRunsPerDay,
|
|
647
|
+
}, "agent_schedule");
|
|
648
|
+
if (result.error)
|
|
649
|
+
return { content: [{ type: "text", text: `Schedule failed: ${result.error}` }], isError: true };
|
|
650
|
+
return {
|
|
651
|
+
content: [{
|
|
652
|
+
type: "text",
|
|
653
|
+
text: [
|
|
654
|
+
`🤖 **Schedule attached to \`${a.name}\`**`,
|
|
655
|
+
``,
|
|
656
|
+
`Workflow: ${a.workflow}${a.topic ? `\nTopic: ${a.topic}` : ""}${a.packetName ? `\nPacket: ${a.packetName}` : ""}`,
|
|
657
|
+
`Cron: ${a.cron}`,
|
|
658
|
+
`Next run: ${result.nextRunIso}`,
|
|
659
|
+
``,
|
|
660
|
+
`The agent will run autonomously. Check progress with \`agent_runs name=${a.name}\`.`,
|
|
661
|
+
].filter(Boolean).join("\n"),
|
|
662
|
+
}],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if (name === "agent_unschedule") {
|
|
666
|
+
const a = args;
|
|
667
|
+
if (!a.name)
|
|
668
|
+
return { content: [{ type: "text", text: "name required" }], isError: true };
|
|
669
|
+
const result = await (0, convex_js_1.callConvex)("/agents/unschedule", "POST", { agentKey: `agent/${a.name}` }, "agent_unschedule");
|
|
670
|
+
if (result.error)
|
|
671
|
+
return { content: [{ type: "text", text: `Unschedule failed: ${result.error}` }], isError: true };
|
|
672
|
+
return {
|
|
673
|
+
content: [{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: result.deleted
|
|
676
|
+
? `🗑️ Schedule removed from \`${a.name}\`. Agent itself remains in vault.`
|
|
677
|
+
: `\`${a.name}\` had no active schedule.`,
|
|
678
|
+
}],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (name === "agent_pause" || name === "agent_resume") {
|
|
682
|
+
const a = args;
|
|
683
|
+
if (!a.name)
|
|
684
|
+
return { content: [{ type: "text", text: "name required" }], isError: true };
|
|
685
|
+
const enabled = name === "agent_resume";
|
|
686
|
+
const result = await (0, convex_js_1.callConvex)("/agents/pause", "POST", { agentKey: `agent/${a.name}`, enabled }, name);
|
|
687
|
+
if (result.error)
|
|
688
|
+
return { content: [{ type: "text", text: `${name} failed: ${result.error}` }], isError: true };
|
|
689
|
+
if (!result.changed)
|
|
690
|
+
return { content: [{ type: "text", text: `\`${a.name}\` has no schedule to ${enabled ? "resume" : "pause"}.` }] };
|
|
691
|
+
return {
|
|
692
|
+
content: [{
|
|
693
|
+
type: "text",
|
|
694
|
+
text: enabled
|
|
695
|
+
? `▶️ Resumed schedule for \`${a.name}\`. Failure counter reset.`
|
|
696
|
+
: `⏸️ Paused schedule for \`${a.name}\`. Use \`agent_resume name=${a.name}\` to re-enable.`,
|
|
697
|
+
}],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (name === "agent_runs") {
|
|
701
|
+
const a = args;
|
|
702
|
+
if (!a.name)
|
|
703
|
+
return { content: [{ type: "text", text: "name required" }], isError: true };
|
|
704
|
+
const limit = Math.min(Math.max(a.limit ?? 10, 1), 50);
|
|
705
|
+
const result = await (0, convex_js_1.callConvex)(`/agents/runs?agentKey=${encodeURIComponent(`agent/${a.name}`)}&limit=${limit}`, "GET", undefined, "agent_runs");
|
|
706
|
+
if (result.error)
|
|
707
|
+
return { content: [{ type: "text", text: `agent_runs failed: ${result.error}` }], isError: true };
|
|
708
|
+
const runs = (result.runs ?? []);
|
|
709
|
+
if (!runs.length) {
|
|
710
|
+
return { content: [{ type: "text", text: `No autonomous runs yet for \`${a.name}\`. The agent runs on its cron schedule - be patient.` }], structuredContent: buildAgentRuns(a.name, []) };
|
|
711
|
+
}
|
|
712
|
+
const lines = [`## Recent runs for \`${a.name}\` (${runs.length})`, ""];
|
|
713
|
+
for (const r of runs) {
|
|
714
|
+
const when = new Date(r.startedAt).toISOString().replace("T", " ").slice(0, 19);
|
|
715
|
+
const dur = r.durationMs ? `${Math.round(r.durationMs / 1000)}s` : "-";
|
|
716
|
+
const icon = r.status === "success" ? "✅" : r.status === "failed" ? "❌" : r.status === "timeout" ? "⏱️" : "⏳";
|
|
717
|
+
lines.push(`${icon} **${when}** · ${r.workflow} · ${dur}${r.toolCallCount != null ? ` · ${r.toolCallCount} tool calls` : ""}`);
|
|
718
|
+
if (r.vaultKeyResult)
|
|
719
|
+
lines.push(` → \`${r.vaultKeyResult}\``);
|
|
720
|
+
if (r.resultSummary)
|
|
721
|
+
lines.push(` ${r.resultSummary.slice(0, 200)}`);
|
|
722
|
+
if (r.errorMsg)
|
|
723
|
+
lines.push(` ⚠️ ${r.errorMsg.slice(0, 200)}`);
|
|
724
|
+
lines.push("");
|
|
725
|
+
}
|
|
726
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildAgentRuns(a.name, runs) };
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|