@kaleidorg/mind 0.8.1 → 0.10.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/README.md +7 -5
- package/dist/bitrefill/index.d.ts +4 -0
- package/dist/bitrefill/index.d.ts.map +1 -0
- package/dist/bitrefill/index.js +3 -0
- package/dist/bitrefill/index.js.map +1 -0
- package/dist/capabilities.d.ts +3 -3
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +4 -4
- package/dist/capabilities.js.map +1 -1
- package/dist/engine/answer.d.ts +37 -0
- package/dist/engine/answer.d.ts.map +1 -0
- package/dist/engine/answer.js +35 -0
- package/dist/engine/answer.js.map +1 -0
- package/dist/engine.d.ts +9 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +159 -175
- package/dist/engine.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/flashnet/index.d.ts +5 -0
- package/dist/flashnet/index.d.ts.map +1 -0
- package/dist/flashnet/index.js +4 -0
- package/dist/flashnet/index.js.map +1 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +20 -0
- package/dist/guards.js.map +1 -1
- package/dist/index.d.ts +7 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -28
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +7 -0
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +71 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/kaleidoswap/index.d.ts +8 -0
- package/dist/kaleidoswap/index.d.ts.map +1 -0
- package/dist/kaleidoswap/index.js +7 -0
- package/dist/kaleidoswap/index.js.map +1 -0
- package/dist/knowledge/index.d.ts +9 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +6 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/lsps1/index.d.ts +4 -0
- package/dist/lsps1/index.d.ts.map +1 -0
- package/dist/lsps1/index.js +3 -0
- package/dist/lsps1/index.js.map +1 -0
- package/dist/providers/types.d.ts +3 -3
- package/dist/providers/types.js +3 -3
- package/dist/qvac/index.d.ts +0 -1
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js +0 -1
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/provider.d.ts +9 -9
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +115 -109
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +4 -3
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/qvac/voice.d.ts +1 -1
- package/dist/recipe/asset-send.js +1 -1
- package/dist/recipe/asset-send.js.map +1 -1
- package/dist/submarine/index.d.ts +5 -0
- package/dist/submarine/index.d.ts.map +1 -0
- package/dist/submarine/index.js +4 -0
- package/dist/submarine/index.js.map +1 -0
- package/dist/testing/mock-wallet.d.ts.map +1 -1
- package/dist/testing/mock-wallet.js +6 -0
- package/dist/testing/mock-wallet.js.map +1 -1
- package/dist/tools/in-process.d.ts +2 -2
- package/dist/tools/in-process.js +2 -2
- package/dist/wallet/contract.d.ts +5 -0
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +39 -6
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +32 -2
- package/scripts/snapshot-mcp-tools.mjs +38 -0
- package/skills/README.md +98 -64
- package/skills/bitrefill/SKILL.md +30 -157
- package/skills/channel-manager/SKILL.md +31 -52
- package/skills/flashnet-swaps/SKILL.md +24 -150
- package/skills/kaleido-node/SKILL.md +25 -55
- package/skills/kaleido-trading/SKILL.md +28 -172
- package/skills/kaleido-trading/references/assets.md +4 -4
- package/skills/kaleido-trading/references/atomic.md +5 -7
- package/skills/merchant-finder/SKILL.md +25 -108
- package/skills/paid-data/SKILL.md +25 -58
- package/skills/portfolio-manager/SKILL.md +26 -60
- package/skills/rgb-lightning-node/SKILL.md +37 -255
- package/skills/rgb-lightning-node/references/channels.md +34 -0
- package/skills/spark-wallet/SKILL.md +26 -228
- package/skills/submarine-swaps/SKILL.md +19 -37
- package/skills/wallet-assistant/SKILL.md +26 -44
- package/src/bitrefill/index.ts +13 -0
- package/src/capabilities.ts +7 -7
- package/src/context/context.test.ts +2 -2
- package/src/engine/answer.ts +66 -0
- package/src/engine.ts +185 -194
- package/src/evidence.ts +1 -1
- package/src/flashnet/index.ts +14 -0
- package/src/funnel.mind.test.ts +6 -5
- package/src/guards.test.ts +12 -0
- package/src/guards.ts +20 -0
- package/src/index.ts +11 -107
- package/src/kaleidoswap/contract.test.ts +32 -3
- package/src/kaleidoswap/contract.ts +65 -17
- package/src/kaleidoswap/index.ts +20 -0
- package/src/knowledge/index.ts +14 -0
- package/src/lsps1/index.ts +13 -0
- package/src/providers/types.ts +3 -3
- package/src/qvac/index.ts +0 -8
- package/src/qvac/provider.test.ts +17 -17
- package/src/qvac/provider.ts +33 -31
- package/src/qvac/stream.ts +4 -3
- package/src/qvac/voice.ts +1 -1
- package/src/recipe/asset-send.ts +1 -1
- package/src/recipe/recipe.test.ts +1 -1
- package/src/skills/catalog.test.ts +215 -0
- package/src/skills/mcp-tools.snapshot.json +1938 -0
- package/src/submarine/index.ts +17 -0
- package/src/testing/mock-wallet.ts +6 -0
- package/src/tools/in-process.ts +2 -2
- package/src/wallet/contract.test.ts +20 -1
- package/src/wallet/contract.ts +36 -6
- package/dist/qvac/delegate.d.ts +0 -50
- package/dist/qvac/delegate.d.ts.map +0 -1
- package/dist/qvac/delegate.js +0 -53
- package/dist/qvac/delegate.js.map +0 -1
- package/skills/dca/SKILL.md +0 -48
- package/skills/kaleido-lsps/SKILL.md +0 -131
- package/skills/liquidity-optimizer/SKILL.md +0 -91
- package/src/qvac/delegate.test.ts +0 -68
- package/src/qvac/delegate.ts +0 -73
package/src/engine.ts
CHANGED
|
@@ -11,27 +11,26 @@
|
|
|
11
11
|
* `{role:'tool'}` results into history each round, loop until the model stops
|
|
12
12
|
* calling tools. Money tools pause for an `onConfirm` gate; their handlers run
|
|
13
13
|
* wherever the ToolSource lives (on the phone for the wallet), even when
|
|
14
|
-
* inference
|
|
14
|
+
* inference runs on a remote server.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { ConfirmDecision, Message, ToolResult } from './types.js';
|
|
18
|
-
import type { LLMProvider } from './providers/types.js';
|
|
19
|
-
import type { InferenceMetrics, ToolCallError, ToolChoice } from './providers/types.js';
|
|
17
|
+
import type { ConfirmDecision, Message, ToolCall, ToolDef, ToolResult } from './types.js';
|
|
18
|
+
import type { InferenceMetrics, LLMProvider, ToolChoice } from './providers/types.js';
|
|
20
19
|
import type { ToolRegistry } from './tools/registry.js';
|
|
21
20
|
import { compressToolResult, type ToolCrushOptions } from './context/compress.js';
|
|
22
|
-
import {
|
|
23
|
-
callKey,
|
|
24
|
-
declinedToolResult,
|
|
25
|
-
detectWalletAction,
|
|
26
|
-
hasCapableTool,
|
|
27
|
-
noToolReply,
|
|
28
|
-
findUngroundedPaymentData,
|
|
29
|
-
fixSatsBtcConversions,
|
|
30
|
-
ungroundedReply,
|
|
31
|
-
validateToolArgs,
|
|
32
|
-
} from './guards.js';
|
|
21
|
+
import { callKey, declinedToolResult, detectWalletAction, hasCapableTool, noToolReply, validateToolArgs } from './guards.js';
|
|
33
22
|
import { confirmReadback } from './wallet/confirm.js';
|
|
34
|
-
import { annotateRgbBalances
|
|
23
|
+
import { annotateRgbBalances } from './context/rgb-units.js';
|
|
24
|
+
import {
|
|
25
|
+
REPEATED_CALL_REPLY,
|
|
26
|
+
TOOL_CALL_FAILED_REPLY,
|
|
27
|
+
cancelledReply,
|
|
28
|
+
engine,
|
|
29
|
+
finalizeAnswer,
|
|
30
|
+
model,
|
|
31
|
+
toolErrorFeedback,
|
|
32
|
+
type Answer,
|
|
33
|
+
} from './engine/answer.js';
|
|
35
34
|
import type { SkillRegistry } from './skills/registry.js';
|
|
36
35
|
import type { Skill } from './skills/types.js';
|
|
37
36
|
import { selectAvailableSkill } from './skills/select.js';
|
|
@@ -125,17 +124,16 @@ export interface AgenticResult {
|
|
|
125
124
|
inference: InferenceMetrics[];
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return `Your tool call could not be read (${detail}). Call the tool again with valid JSON arguments that match its schema, or ask the user for the missing values.`;
|
|
127
|
+
/** State of one agentic run. */
|
|
128
|
+
interface RunState {
|
|
129
|
+
history: Message[];
|
|
130
|
+
system?: string;
|
|
131
|
+
tools: ToolDef[];
|
|
132
|
+
executed: ToolResult[];
|
|
133
|
+
inference: InferenceMetrics[];
|
|
134
|
+
/** Calls made this run, by name + arguments, with their first result. */
|
|
135
|
+
seen: Map<string, { result: unknown; count: number }>;
|
|
136
|
+
lastRequestId?: string;
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
export class Engine {
|
|
@@ -190,226 +188,219 @@ export class Engine {
|
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
private async runAgenticSession(messages: Message[], opts: AgenticOptions): Promise<AgenticResult> {
|
|
193
|
-
const maxTurns = opts.maxTurns ?? this.defaultMaxTurns;
|
|
194
|
-
const hasSystem = messages.some((m) => m.role === 'system');
|
|
195
|
-
const system = hasSystem ? undefined : this.defaultSystem;
|
|
196
|
-
|
|
197
191
|
const startedAt = Date.now();
|
|
198
|
-
const
|
|
192
|
+
const maxTurns = opts.maxTurns ?? this.defaultMaxTurns;
|
|
199
193
|
const registryTools = await this.registry.listTools();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
let engineReply = false;
|
|
210
|
-
let turns = 0;
|
|
211
|
-
const inference: InferenceMetrics[] = [];
|
|
212
|
-
const seen = new Map<string, { result: unknown; count: number }>();
|
|
213
|
-
let toolErrorRetries = 0;
|
|
194
|
+
const state: RunState = {
|
|
195
|
+
history: [...messages],
|
|
196
|
+
system: messages.some((m) => m.role === 'system') ? undefined : this.defaultSystem,
|
|
197
|
+
// Progressive disclosure: expose only the active skill's tools when set.
|
|
198
|
+
tools: opts.allowedTools ? registryTools.filter((t) => opts.allowedTools!.includes(t.name)) : registryTools,
|
|
199
|
+
executed: [],
|
|
200
|
+
inference: [],
|
|
201
|
+
seen: new Map(),
|
|
202
|
+
};
|
|
214
203
|
|
|
215
204
|
const lastUser = [...messages].reverse().find((m) => m.role === 'user')?.content ?? '';
|
|
216
205
|
const action = this.guardMissingTools ? detectWalletAction(lastUser) : null;
|
|
217
|
-
if (action && !hasCapableTool(action,
|
|
206
|
+
if (action && !hasCapableTool(action, state.tools.map((t) => t.name))) {
|
|
218
207
|
const text = noToolReply(action);
|
|
219
|
-
history.push({ role: 'assistant', content: text });
|
|
220
|
-
return { text, turns: 0, toolCalls: [], messages: history, latencyMs: Date.now() - startedAt, inference };
|
|
208
|
+
state.history.push({ role: 'assistant', content: text });
|
|
209
|
+
return { text, turns: 0, toolCalls: [], messages: state.history, latencyMs: Date.now() - startedAt, inference: state.inference };
|
|
221
210
|
}
|
|
222
211
|
|
|
212
|
+
let answer: Answer = model('');
|
|
213
|
+
let turns = 0;
|
|
214
|
+
let toolErrorRetries = 0;
|
|
223
215
|
// A retry after an unreadable tool call does not count against maxTurns.
|
|
224
216
|
for (let turn = 1; turn <= maxTurns + toolErrorRetries; turn++) {
|
|
225
217
|
turns = turn;
|
|
226
218
|
if (opts.signal?.aborted) break;
|
|
227
219
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// there costs most of the turn's time on small models.
|
|
235
|
-
...(turn === 1 && opts.firstTurnToolChoice && allTools.length
|
|
236
|
-
? { toolChoice: opts.firstTurnToolChoice, ...(this.thinkOnForcedCalls ? {} : { thinking: 'off' as const }) }
|
|
237
|
-
: {}),
|
|
238
|
-
onToken: opts.onToken ? (t) => opts.onToken!(t, turn) : undefined,
|
|
239
|
-
signal: opts.signal,
|
|
220
|
+
// A forced first call only picks the tool and its arguments; reasoning
|
|
221
|
+
// there costs most of the turn's time on small models.
|
|
222
|
+
const forced = turn === 1 && opts.firstTurnToolChoice && state.tools.length;
|
|
223
|
+
const out = await this.callModel(state, opts, turn, {
|
|
224
|
+
tools: state.tools,
|
|
225
|
+
...(forced ? { toolChoice: opts.firstTurnToolChoice, ...(this.thinkOnForcedCalls ? {} : { thinking: 'off' as const }) } : {}),
|
|
240
226
|
});
|
|
241
|
-
|
|
242
|
-
lastRequestId = out.requestId;
|
|
243
|
-
if (out.inference) inference.push(out.inference);
|
|
244
227
|
if (out.requestId) opts.onStart?.(out.requestId, turn);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
toolErrorRetries
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
228
|
+
answer = model(out.incomplete ? '' : (out.text || '').trim());
|
|
229
|
+
|
|
230
|
+
if (!out.toolCalls?.length) {
|
|
231
|
+
// The model tried to call a tool but the call didn't parse: tell it
|
|
232
|
+
// what went wrong and let it try once more instead of showing the
|
|
233
|
+
// broken frame as the answer.
|
|
234
|
+
if (out.toolErrors?.length) {
|
|
235
|
+
if (toolErrorRetries < 1) {
|
|
236
|
+
toolErrorRetries += 1;
|
|
237
|
+
state.history.push({ role: 'assistant', content: out.rawContent || answer.text });
|
|
238
|
+
state.history.push({
|
|
239
|
+
role: 'tool',
|
|
240
|
+
content: JSON.stringify({ error: toolErrorFeedback(out.toolErrors, out.inference?.status === 'truncated') }),
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
answer = engine(TOOL_CALL_FAILED_REPLY);
|
|
245
|
+
break;
|
|
257
246
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// No tool calls ⇒ the model produced its final answer.
|
|
264
|
-
if (!out.toolCalls || out.toolCalls.length === 0) {
|
|
265
|
-
if (!finalText && executed.length) finalText = await this.recoverAnswer(history, system, executed, inference, opts, turn);
|
|
266
|
-
else if (!finalText && out.incomplete) finalText = (out.text || '').trim();
|
|
247
|
+
// No tool calls ⇒ the model produced its final answer.
|
|
248
|
+
if (!answer.text && state.executed.length) answer = await this.recoverAnswer(state, opts, turn);
|
|
249
|
+
else if (!answer.text && out.incomplete) answer = model((out.text || '').trim());
|
|
267
250
|
break;
|
|
268
251
|
}
|
|
269
252
|
|
|
270
253
|
// Anchor the next turn with the raw assistant frame.
|
|
271
|
-
history.push({ role: 'assistant', content: out.rawContent ||
|
|
254
|
+
state.history.push({ role: 'assistant', content: out.rawContent || answer.text });
|
|
272
255
|
|
|
273
256
|
let repeatedAgain = false;
|
|
274
|
-
const
|
|
257
|
+
const declined: string[] = [];
|
|
275
258
|
for (const call of out.toolCalls) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const previous = seen.get(key);
|
|
280
|
-
|
|
281
|
-
let args = call.arguments;
|
|
282
|
-
let result: unknown;
|
|
283
|
-
if (previous) {
|
|
284
|
-
previous.count += 1;
|
|
285
|
-
if (previous.count > 2) repeatedAgain = true;
|
|
286
|
-
result = {
|
|
287
|
-
error:
|
|
288
|
-
`You already called ${call.name} with these arguments; the result was: ` +
|
|
289
|
-
`${this.toHistoryContent(previous.result)}. Do not call it again — answer the user now.`,
|
|
290
|
-
};
|
|
291
|
-
} else if (!def) {
|
|
292
|
-
result = { error: `Unknown tool "${call.name}".` };
|
|
293
|
-
} else {
|
|
294
|
-
const check = validateToolArgs(def, call.arguments);
|
|
295
|
-
if (!check.ok) {
|
|
296
|
-
result = {
|
|
297
|
-
error: `Invalid arguments for ${call.name}: ${check.errors.join('; ')}. Fix them or ask the user for the missing values.`,
|
|
298
|
-
};
|
|
299
|
-
} else if (def.requiresConfirmation) {
|
|
300
|
-
args = check.args;
|
|
301
|
-
const summary = confirmReadback({ name: call.name, arguments: args }) ?? undefined;
|
|
302
|
-
const decision = opts.onConfirm
|
|
303
|
-
? await opts.onConfirm({ name: call.name, arguments: args, ...(summary ? { summary } : {}) })
|
|
304
|
-
: { approved: false, reason: 'no confirmation handler available' };
|
|
305
|
-
if (decision.approved) {
|
|
306
|
-
result = await this.safeExecute(call.name, args);
|
|
307
|
-
} else {
|
|
308
|
-
result = declinedToolResult(call.name, decision.reason);
|
|
309
|
-
declinedThisTurn.push(summary ? summary.replace(/\.?\s*Confirm\?$/, '') : call.name.replace(/_/g, ' '));
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
args = check.args;
|
|
313
|
-
result = await this.safeExecute(call.name, args);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (!previous) {
|
|
318
|
-
// A mutating (confirm-gated) call can change what reads return.
|
|
319
|
-
if (def?.requiresConfirmation) seen.clear();
|
|
320
|
-
seen.set(key, { result, count: 1 });
|
|
321
|
-
}
|
|
322
|
-
executed.push({ name: call.name, arguments: args, result });
|
|
323
|
-
opts.onToolResult?.({ name: call.name, arguments: args, result }, turn);
|
|
324
|
-
history.push({ role: 'tool', content: this.toHistoryContent(this.fixAmounts ? annotateRgbBalances(result) : result) });
|
|
259
|
+
const step = await this.executeCall(state, call, opts, turn);
|
|
260
|
+
repeatedAgain ||= step.repeatedAgain;
|
|
261
|
+
if (step.declined) declined.push(step.declined);
|
|
325
262
|
}
|
|
326
263
|
|
|
327
|
-
if (this.endTurnOnDecline &&
|
|
328
|
-
|
|
329
|
-
engineReply = true;
|
|
264
|
+
if (this.endTurnOnDecline && declined.length && declined.length === out.toolCalls.length) {
|
|
265
|
+
answer = engine(cancelledReply(declined));
|
|
330
266
|
break;
|
|
331
267
|
}
|
|
332
268
|
|
|
333
269
|
if (repeatedAgain) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
tools: [],
|
|
338
|
-
system,
|
|
339
|
-
onToken: opts.onToken ? (t) => opts.onToken!(t, turn) : undefined,
|
|
340
|
-
signal: opts.signal,
|
|
341
|
-
});
|
|
342
|
-
if (forced.inference) inference.push(forced.inference);
|
|
343
|
-
finalText = (forced.text || '').trim() || 'I could not get a different result from the wallet — please try a more specific request.';
|
|
270
|
+
const last = await this.callModel(state, opts, turn, { tools: [] });
|
|
271
|
+
const text = (last.text || '').trim();
|
|
272
|
+
answer = text ? model(text) : engine(REPEATED_CALL_REPLY);
|
|
344
273
|
break;
|
|
345
274
|
}
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Never return an empty answer (e.g. the last turn ran out of tokens).
|
|
350
|
-
if (!finalText && !opts.signal?.aborted) {
|
|
351
|
-
finalText = STOPPED_MESSAGE;
|
|
352
|
-
engineReply = true;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (this.fixAmounts && finalText && !engineReply) {
|
|
356
|
-
finalText = fixRgbBalanceUnits(fixSatsBtcConversions(finalText), executed.map((e) => e.result));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (this.guardPaymentData && finalText && !engineReply) {
|
|
360
|
-
const ungrounded = findUngroundedPaymentData(finalText, [
|
|
361
|
-
...messages.map((m) => m.content),
|
|
362
|
-
...executed.map((e) => e.result),
|
|
363
|
-
]);
|
|
364
|
-
if (ungrounded.length) finalText = ungroundedReply(ungrounded);
|
|
365
275
|
}
|
|
366
276
|
|
|
277
|
+
const text = finalizeAnswer(answer, {
|
|
278
|
+
fixAmounts: this.fixAmounts,
|
|
279
|
+
guardPaymentData: this.guardPaymentData,
|
|
280
|
+
sources: [...messages.map((m) => m.content), ...state.executed.map((e) => e.result)],
|
|
281
|
+
toolResults: state.executed.map((e) => e.result),
|
|
282
|
+
aborted: !!opts.signal?.aborted,
|
|
283
|
+
});
|
|
367
284
|
// Append the final answer so the returned conversation is complete (the
|
|
368
285
|
// loop breaks before pushing the no-tool-call turn).
|
|
369
|
-
if (
|
|
286
|
+
if (text) state.history.push({ role: 'assistant', content: text });
|
|
370
287
|
|
|
371
288
|
return {
|
|
372
|
-
text
|
|
289
|
+
text,
|
|
373
290
|
turns,
|
|
374
|
-
toolCalls: executed,
|
|
375
|
-
requestId: lastRequestId,
|
|
376
|
-
messages: history,
|
|
291
|
+
toolCalls: state.executed,
|
|
292
|
+
requestId: state.lastRequestId,
|
|
293
|
+
messages: state.history,
|
|
377
294
|
latencyMs: Date.now() - startedAt,
|
|
378
|
-
inference,
|
|
295
|
+
inference: state.inference,
|
|
379
296
|
};
|
|
380
297
|
}
|
|
381
298
|
|
|
299
|
+
/** One model call within the run's session; records its receipt. */
|
|
300
|
+
private async callModel(
|
|
301
|
+
state: RunState,
|
|
302
|
+
opts: AgenticOptions,
|
|
303
|
+
turn: number,
|
|
304
|
+
extra: { tools: ToolDef[]; messages?: Message[]; toolChoice?: ToolChoice; thinking?: 'off' },
|
|
305
|
+
) {
|
|
306
|
+
const out = await this.provider.runTurn({
|
|
307
|
+
messages: extra.messages ?? state.history,
|
|
308
|
+
system: state.system,
|
|
309
|
+
sessionKey: opts.sessionKey,
|
|
310
|
+
...extra,
|
|
311
|
+
onToken: opts.onToken ? (t) => opts.onToken!(t, turn) : undefined,
|
|
312
|
+
signal: opts.signal,
|
|
313
|
+
});
|
|
314
|
+
if (out.inference) state.inference.push(out.inference);
|
|
315
|
+
if (out.requestId) state.lastRequestId = out.requestId;
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Validate, confirm (for spends) and execute one tool call, and add its
|
|
321
|
+
* result to the history. Returns the readback when the user declined it.
|
|
322
|
+
*/
|
|
323
|
+
private async executeCall(
|
|
324
|
+
state: RunState,
|
|
325
|
+
call: ToolCall,
|
|
326
|
+
opts: AgenticOptions,
|
|
327
|
+
turn: number,
|
|
328
|
+
): Promise<{ repeatedAgain: boolean; declined?: string }> {
|
|
329
|
+
opts.onToolCall?.({ name: call.name, arguments: call.arguments }, turn);
|
|
330
|
+
const def = await this.registry.getDef(call.name);
|
|
331
|
+
const key = callKey(call.name, call.arguments);
|
|
332
|
+
const previous = state.seen.get(key);
|
|
333
|
+
let repeatedAgain = false;
|
|
334
|
+
let declined: string | undefined;
|
|
335
|
+
|
|
336
|
+
let args = call.arguments;
|
|
337
|
+
let result: unknown;
|
|
338
|
+
if (previous) {
|
|
339
|
+
previous.count += 1;
|
|
340
|
+
if (previous.count > 2) repeatedAgain = true;
|
|
341
|
+
result = {
|
|
342
|
+
error:
|
|
343
|
+
`You already called ${call.name} with these arguments; the result was: ` +
|
|
344
|
+
`${this.toHistoryContent(previous.result)}. Do not call it again — answer the user now.`,
|
|
345
|
+
};
|
|
346
|
+
} else if (!def) {
|
|
347
|
+
result = { error: `Unknown tool "${call.name}".` };
|
|
348
|
+
} else {
|
|
349
|
+
const check = validateToolArgs(def, call.arguments);
|
|
350
|
+
if (!check.ok) {
|
|
351
|
+
result = {
|
|
352
|
+
error: `Invalid arguments for ${call.name}: ${check.errors.join('; ')}. Fix them or ask the user for the missing values.`,
|
|
353
|
+
};
|
|
354
|
+
} else if (def.requiresConfirmation) {
|
|
355
|
+
args = check.args;
|
|
356
|
+
const summary = confirmReadback({ name: call.name, arguments: args }) ?? undefined;
|
|
357
|
+
const decision = opts.onConfirm
|
|
358
|
+
? await opts.onConfirm({ name: call.name, arguments: args, ...(summary ? { summary } : {}) })
|
|
359
|
+
: { approved: false, reason: 'no confirmation handler available' };
|
|
360
|
+
if (decision.approved) {
|
|
361
|
+
result = await this.safeExecute(call.name, args);
|
|
362
|
+
} else {
|
|
363
|
+
result = declinedToolResult(call.name, decision.reason);
|
|
364
|
+
declined = summary ? summary.replace(/\.?\s*Confirm\?$/, '') : call.name.replace(/_/g, ' ');
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
args = check.args;
|
|
368
|
+
result = await this.safeExecute(call.name, args);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!previous) {
|
|
373
|
+
// A mutating (confirm-gated) call can change what reads return.
|
|
374
|
+
if (def?.requiresConfirmation) state.seen.clear();
|
|
375
|
+
state.seen.set(key, { result, count: 1 });
|
|
376
|
+
}
|
|
377
|
+
state.executed.push({ name: call.name, arguments: args, result });
|
|
378
|
+
opts.onToolResult?.({ name: call.name, arguments: args, result }, turn);
|
|
379
|
+
state.history.push({ role: 'tool', content: this.toHistoryContent(this.fixAmounts ? annotateRgbBalances(result) : result) });
|
|
380
|
+
return { repeatedAgain, ...(declined ? { declined } : {}) };
|
|
381
|
+
}
|
|
382
|
+
|
|
382
383
|
/**
|
|
383
384
|
* The model ran tools but produced no visible answer (e.g. reasoning used the
|
|
384
385
|
* whole output budget). Ask once more without tools; if that is empty too,
|
|
385
386
|
* show the last tool result instead of an empty reply.
|
|
386
387
|
*/
|
|
387
|
-
private async recoverAnswer(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
inference: InferenceMetrics[],
|
|
392
|
-
opts: AgenticOptions,
|
|
393
|
-
turn: number,
|
|
394
|
-
): Promise<string> {
|
|
395
|
-
if (opts.signal?.aborted) return '';
|
|
396
|
-
const retry = await this.provider.runTurn({
|
|
397
|
-
sessionKey: opts.sessionKey,
|
|
388
|
+
private async recoverAnswer(state: RunState, opts: AgenticOptions, turn: number): Promise<Answer> {
|
|
389
|
+
if (opts.signal?.aborted) return model('');
|
|
390
|
+
const retry = await this.callModel(state, opts, turn, {
|
|
391
|
+
tools: [],
|
|
398
392
|
messages: [
|
|
399
|
-
...history,
|
|
393
|
+
...state.history,
|
|
400
394
|
{ role: 'user', content: 'Answer my question now from the tool results above, in a few short sentences.' },
|
|
401
395
|
],
|
|
402
|
-
tools: [],
|
|
403
|
-
system,
|
|
404
|
-
onToken: opts.onToken ? (t) => opts.onToken!(t, turn) : undefined,
|
|
405
|
-
signal: opts.signal,
|
|
406
396
|
});
|
|
407
|
-
if (retry.inference) inference.push(retry.inference);
|
|
408
397
|
const text = retry.incomplete ? '' : (retry.text || '').trim();
|
|
409
|
-
if (text) return text;
|
|
410
|
-
const last = executed[executed.length - 1]!;
|
|
398
|
+
if (text) return model(text);
|
|
399
|
+
const last = state.executed[state.executed.length - 1]!;
|
|
411
400
|
const body = compressToolResult(last.result, this.compressOpts ?? {}).content;
|
|
412
|
-
return
|
|
401
|
+
return engine(
|
|
402
|
+
`I couldn't phrase an answer in time. Here is what ${last.name.replace(/_/g, ' ')} returned:\n\n${body.length > 2000 ? `${body.slice(0, 2000)}…` : body}`,
|
|
403
|
+
);
|
|
413
404
|
}
|
|
414
405
|
|
|
415
406
|
async cancel(requestId: string): Promise<void> {
|
package/src/evidence.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Flashnet (Spark-native AMM): tool contract and swap recipe. */
|
|
2
|
+
export {
|
|
3
|
+
FLASHNET_TOOLS,
|
|
4
|
+
FLASHNET_SPEND_TOOLS,
|
|
5
|
+
isFlashnetSpendTool,
|
|
6
|
+
getFlashnetTool,
|
|
7
|
+
bindFlashnetTools,
|
|
8
|
+
} from './contract.js';
|
|
9
|
+
export type {
|
|
10
|
+
FlashnetToolDef,
|
|
11
|
+
FlashnetHandler,
|
|
12
|
+
BindFlashnetOptions,
|
|
13
|
+
} from './contract.js';
|
|
14
|
+
export { flashnetSwapRecipe } from '../recipe/flashnet-swap.js';
|
package/src/funnel.mind.test.ts
CHANGED
|
@@ -318,10 +318,11 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
it('wallet-assistant
|
|
321
|
+
it('wallet-assistant is the in-app router; the node skill carries the rln_* balance tools', () => {
|
|
322
322
|
const wallet = SKILLS.find((s) => s.name === 'wallet-assistant')!;
|
|
323
|
-
expect(wallet.tools).
|
|
324
|
-
|
|
323
|
+
expect(wallet.metadata?.['requires-tools']).toBe('get_balances');
|
|
324
|
+
const node = SKILLS.find((s) => s.name === 'rgb-lightning-node')!;
|
|
325
|
+
expect(node.tools).toEqual(expect.arrayContaining(['rln_get_balances', 'rln_get_address', 'rln_send_btc', 'rln_create_ln_invoice']));
|
|
325
326
|
});
|
|
326
327
|
|
|
327
328
|
it('rgb-lightning-node (triggers on "channels") exposes only canonical rln_* tools', () => {
|
|
@@ -361,9 +362,9 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
361
362
|
const res = await funnel.runTurn("what's my balance?");
|
|
362
363
|
|
|
363
364
|
expect(res.tier).toBe('agentic');
|
|
364
|
-
//
|
|
365
|
+
// Node-only host: the RGB node skill is selected AND rln_get_balances survives its scoping…
|
|
365
366
|
const agenticLine = logs.find((l) => l.startsWith('tier=agentic'));
|
|
366
|
-
expect(agenticLine).toMatch(/skill=
|
|
367
|
+
expect(agenticLine).toMatch(/skill=rgb-lightning-node/);
|
|
367
368
|
expect(agenticLine).toMatch(/rln_get_balances/);
|
|
368
369
|
// …and the tool actually executes (not narrated).
|
|
369
370
|
expect(calls.map((c) => c.name)).toContain('rln_get_balances');
|
package/src/guards.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { InProcessToolSource } from './tools/in-process.js';
|
|
|
6
6
|
import { parseSkill, SkillRegistry } from './skills/registry.js';
|
|
7
7
|
import { scriptedProvider } from './testing/scripted-provider.js';
|
|
8
8
|
import { confirmReadback } from './wallet/confirm.js';
|
|
9
|
+
import { getKaleidoswapTool } from './kaleidoswap/contract.js';
|
|
9
10
|
import {
|
|
10
11
|
DECLINED_TOOL_MESSAGE,
|
|
11
12
|
detectWalletAction,
|
|
@@ -31,7 +32,18 @@ const ISSUE_SCHEMA = {
|
|
|
31
32
|
|
|
32
33
|
const INVOICE = 'lnbcrt50u1pn9xyzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq';
|
|
33
34
|
|
|
35
|
+
const QUOTE = { name: 'kaleidoswap_get_quote', parameters: getKaleidoswapTool('kaleidoswap_get_quote')!.parameters };
|
|
36
|
+
|
|
34
37
|
describe('validateToolArgs', () => {
|
|
38
|
+
it('rejects sats passed as a BTC display amount and names the BTC value', () => {
|
|
39
|
+
const r = validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 50_000 });
|
|
40
|
+
expect(r.ok).toBe(false);
|
|
41
|
+
expect(r.errors.join(' ')).toMatch(/from_amount: 0\.0005/);
|
|
42
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.0005 }).ok).toBe(true);
|
|
43
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'USDT', to_asset_id: 'BTC', from_amount: 5000 }).ok).toBe(true);
|
|
44
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 1, to_amount: 2 }).ok).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
35
47
|
it('accepts valid args and coerces numeric strings', () => {
|
|
36
48
|
const r = validateToolArgs({ name: 'rln_issue_asset', parameters: ISSUE_SCHEMA }, { name: 'Hack', ticker: 'HCK', amount: '1000' });
|
|
37
49
|
expect(r.ok).toBe(true);
|
package/src/guards.ts
CHANGED
|
@@ -78,6 +78,26 @@ const SEMANTIC_RULES: Record<string, (a: Record<string, unknown>) => string[]> =
|
|
|
78
78
|
};
|
|
79
79
|
SEMANTIC_RULES.wdk_issue_asset = SEMANTIC_RULES.rln_issue_asset!;
|
|
80
80
|
|
|
81
|
+
/** Above this, a BTC amount in display units is almost certainly sats. */
|
|
82
|
+
const MAX_PLAUSIBLE_BTC = 1000;
|
|
83
|
+
|
|
84
|
+
/** KaleidoSwap quote amounts are display units; reject sats passed as BTC. */
|
|
85
|
+
function btcDisplayAmounts(a: Record<string, unknown>): string[] {
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
const legs: Array<[string, unknown]> = [['from_amount', a.from_asset_id], ['to_amount', a.to_asset_id]];
|
|
88
|
+
if (a.from_amount != null && a.to_amount != null) errors.push('give only one of "from_amount" or "to_amount"');
|
|
89
|
+
for (const [key, asset] of legs) {
|
|
90
|
+
const v = a[key];
|
|
91
|
+
if (typeof v !== 'number' || !/^btc$/i.test(String(asset ?? '').trim())) continue;
|
|
92
|
+
if (v >= MAX_PLAUSIBLE_BTC) {
|
|
93
|
+
errors.push(`"${key}" is in BTC, not sats: ${v} sats is ${+(v / 1e8).toFixed(8)} BTC — pass ${key}: ${+(v / 1e8).toFixed(8)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
SEMANTIC_RULES.kaleidoswap_get_quote = btcDisplayAmounts;
|
|
99
|
+
SEMANTIC_RULES.kaleidoswap_get_spreads = btcDisplayAmounts;
|
|
100
|
+
|
|
81
101
|
/**
|
|
82
102
|
* Validate (and lightly coerce) a tool call's arguments against its schema.
|
|
83
103
|
* Unknown schema shapes pass through unchanged.
|