talon-agent 3.7.0 → 3.8.1
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 +16 -0
- package/package.json +2 -2
- package/prompts/README.md +10 -2
- package/prompts/dream.md +5 -3
- package/prompts/heartbeat.md +1 -1
- package/prompts/identity.md +1 -1
- package/prompts/mem0.md +7 -5
- package/prompts/mempalace.md +7 -5
- package/prompts/system/memory-recall.md +49 -0
- package/prompts/system/workspace.md +2 -2
- package/src/backend/claude-sdk/one-shot.ts +31 -1
- package/src/backend/codex/effort.ts +30 -0
- package/src/backend/codex/handler/message.ts +12 -22
- package/src/backend/codex/one-shot.ts +20 -0
- package/src/backend/opencode/handler/message.ts +7 -13
- package/src/backend/remote-server/one-shot.ts +6 -0
- package/src/backend/shared/handler-to-events.ts +0 -1
- package/src/backend/shared/handler-types.ts +0 -8
- package/src/backend/shared/index.ts +1 -6
- package/src/backend/shared/prompt-format.ts +0 -69
- package/src/bootstrap.ts +2 -0
- package/src/core/agent-runtime/capabilities.ts +5 -49
- package/src/core/background/dream.ts +27 -2
- package/src/core/background/effort.ts +80 -0
- package/src/core/background/heartbeat/agent.ts +22 -1
- package/src/core/background/heartbeat/state.ts +7 -0
- package/src/core/engine/model-audit.ts +69 -2
- package/src/core/errors.ts +107 -3
- package/src/core/models/reasoning-levels.ts +14 -3
- package/src/core/prompt/assemble.ts +8 -5
- package/src/core/prompt/embedded-prompts.ts +16 -14
- package/src/core/types.ts +10 -0
- package/src/core/weaver/index.ts +0 -1
- package/src/core/weaver/weaver.ts +1 -24
- package/src/frontend/telegram/callbacks/index.ts +8 -0
- package/src/frontend/telegram/callbacks/metrics.ts +36 -0
- package/src/frontend/telegram/commands/admin.ts +9 -13
- package/src/frontend/telegram/commands/info.ts +3 -44
- package/src/frontend/telegram/helpers/diagnostics.ts +190 -70
- package/src/util/config.ts +34 -19
- package/src/core/memory/retrieval.ts +0 -92
- package/src/core/weaver/memory-prefetch.ts +0 -65
package/src/util/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirs, files as pathFiles } from "./paths.js";
|
|
|
5
5
|
import { hardenTalonPermissions } from "./harden.js";
|
|
6
6
|
import { setTimezone } from "./time.js";
|
|
7
7
|
import { BACKEND_IDS } from "../core/agent-runtime/model-ref.js";
|
|
8
|
+
import { REASONING_LEVEL_ORDER } from "../core/models/reasoning-levels.js";
|
|
8
9
|
import {
|
|
9
10
|
assembleSystemPrompt,
|
|
10
11
|
joinSystemPromptParts,
|
|
@@ -28,6 +29,20 @@ const BACKEND_ID_ENUM = [...BACKEND_IDS] as [
|
|
|
28
29
|
...(typeof BACKEND_IDS)[number][],
|
|
29
30
|
];
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Reasoning-effort literal source for the background-agent knobs
|
|
34
|
+
* (`heartbeatEffort` / `dreamEffort`). Same trick as `BACKEND_ID_ENUM`:
|
|
35
|
+
* reuse the single source of truth (`REASONING_LEVEL_ORDER`) so adding a
|
|
36
|
+
* level to the vocabulary doesn't need a second edit here.
|
|
37
|
+
*
|
|
38
|
+
* There is no `"adaptive"` member — leaving the field unset IS adaptive
|
|
39
|
+
* (the backend/model default), matching how per-chat `effort` behaves.
|
|
40
|
+
*/
|
|
41
|
+
const REASONING_EFFORT_ENUM = [...REASONING_LEVEL_ORDER] as [
|
|
42
|
+
(typeof REASONING_LEVEL_ORDER)[number],
|
|
43
|
+
...(typeof REASONING_LEVEL_ORDER)[number][],
|
|
44
|
+
];
|
|
45
|
+
|
|
31
46
|
// ── Config schema ───────────────────────────────────────────────────────────
|
|
32
47
|
|
|
33
48
|
/** Path-based Talon plugin (loaded as a Node module). */
|
|
@@ -241,23 +256,6 @@ const mem0SettingsSchema = z.object({
|
|
|
241
256
|
userId: z.string().min(1).optional(),
|
|
242
257
|
});
|
|
243
258
|
|
|
244
|
-
/**
|
|
245
|
-
* Memory pre-retrieval (Phase B) — automatic per-turn palace retrieval.
|
|
246
|
-
* Ships INERT: the flag gates wiring a real retriever into the dispatcher.
|
|
247
|
-
* With `enabled: false` (default) prompts are byte-identical to previous
|
|
248
|
-
* behavior. See docs in core/memory/retrieval.ts for the trust policy.
|
|
249
|
-
*/
|
|
250
|
-
const memoryPreRetrievalSchema = z.object({
|
|
251
|
-
/** Master switch. Default off — plumbing merges disabled. */
|
|
252
|
-
enabled: z.boolean().default(false),
|
|
253
|
-
/** Maximum retrieved items injected per turn. */
|
|
254
|
-
maxResults: z.number().int().min(1).max(10).default(3),
|
|
255
|
-
/** Hard cap on injected characters, provenance labels included. */
|
|
256
|
-
maxChars: z.number().int().min(200).max(20000).default(3000),
|
|
257
|
-
/** In groups, filter private-user drawers before injection. */
|
|
258
|
-
groupPrivateUserFiltering: z.boolean().default(true),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
259
|
const configSchema = z.object({
|
|
262
260
|
frontend: z.union([frontendEnum, z.array(frontendEnum)]).default("telegram"),
|
|
263
261
|
botToken: z.string().optional(),
|
|
@@ -317,6 +315,15 @@ const configSchema = z.object({
|
|
|
317
315
|
*/
|
|
318
316
|
backendDefaults: z.record(z.string(), z.string()).optional(),
|
|
319
317
|
dreamModel: z.string().optional(), // Model used for background memory consolidation (defaults to main model)
|
|
318
|
+
/**
|
|
319
|
+
* Reasoning effort for the dream / memory-consolidation agent. Unset =
|
|
320
|
+
* the backend/model default. Pair with `dreamModel` when you want a
|
|
321
|
+
* cheap model that still thinks hard (or an expensive one that doesn't).
|
|
322
|
+
*
|
|
323
|
+
* Honoured by backends with a reasoning knob (Claude SDK, Codex);
|
|
324
|
+
* silently ignored by Kilo / OpenCode, which have none.
|
|
325
|
+
*/
|
|
326
|
+
dreamEffort: z.enum(REASONING_EFFORT_ENUM).optional(),
|
|
320
327
|
maxMessageLength: z.number().int().min(100).default(4000),
|
|
321
328
|
concurrency: z.number().int().min(1).max(20).default(1),
|
|
322
329
|
apiId: z.number().int().optional(),
|
|
@@ -327,8 +334,6 @@ const configSchema = z.object({
|
|
|
327
334
|
pulseIntervalMs: z.number().int().min(60000).default(300000),
|
|
328
335
|
/** Background memory-consolidation (dream) runs. Mirrors `pulse`/`heartbeat`. */
|
|
329
336
|
dream: z.boolean().default(true),
|
|
330
|
-
/** Memory pre-retrieval (Phase B). Optional; absent = disabled. */
|
|
331
|
-
memoryPreRetrieval: memoryPreRetrievalSchema.optional(),
|
|
332
337
|
/**
|
|
333
338
|
* Periodic background agent (default: on, hourly). Advances open
|
|
334
339
|
* goals, runs user-defined maintenance, and proactively messages
|
|
@@ -339,6 +344,16 @@ const configSchema = z.object({
|
|
|
339
344
|
heartbeat: z.boolean().default(true),
|
|
340
345
|
heartbeatIntervalMinutes: z.number().int().min(5).default(60),
|
|
341
346
|
heartbeatModel: z.string().optional(), // Model for heartbeat agent (defaults to main model)
|
|
347
|
+
/**
|
|
348
|
+
* Reasoning effort for the heartbeat agent. Unset = the backend/model
|
|
349
|
+
* default. Pair with `heartbeatModel` — e.g. `"high"` so unattended
|
|
350
|
+
* goal work reasons harder than a chat turn, or `"low"` to keep hourly
|
|
351
|
+
* runs cheap.
|
|
352
|
+
*
|
|
353
|
+
* Honoured by backends with a reasoning knob (Claude SDK, Codex);
|
|
354
|
+
* silently ignored by Kilo / OpenCode, which have none.
|
|
355
|
+
*/
|
|
356
|
+
heartbeatEffort: z.enum(REASONING_EFFORT_ENUM).optional(),
|
|
342
357
|
braveApiKey: z.string().optional(),
|
|
343
358
|
/**
|
|
344
359
|
* Codex-specific OpenAI API key. Prefer this, CODEX_API_KEY, or
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory pre-retrieval (Phase B) — injectable retriever boundary.
|
|
3
|
-
*
|
|
4
|
-
* Phase A made `memory.md` the compact always-loaded layer; Phase B closes the
|
|
5
|
-
* recall gap by handing each chat turn a small, relevant palace slice without
|
|
6
|
-
* depending on the model to call `mempalace_search` itself. This module owns
|
|
7
|
-
* the CORE side of that boundary: a typed retriever dependency the Weaver can
|
|
8
|
-
* call before `runChatTurn(...)`, returning plain data
|
|
9
|
-
* (`RetrievedMemory | undefined`) that backends fold into the live user
|
|
10
|
-
* prompt via `formatPromptWithRetrievedMemory`.
|
|
11
|
-
*
|
|
12
|
-
* Deliberate constraints (see docs/memory-phase-b-pre-retrieval.md):
|
|
13
|
-
*
|
|
14
|
-
* - The retriever receives a small context object and returns plain data.
|
|
15
|
-
* It never imports backend handlers, and retrieval output never enters
|
|
16
|
-
* `prepareSystemPrompt()` / frozen prompt snapshots — cache safety first.
|
|
17
|
-
* - Fail closed: a broken retriever must never block chat delivery. The
|
|
18
|
-
* Weaver catches errors, logs one warning, and runs the turn without
|
|
19
|
-
* injected memory.
|
|
20
|
-
* - The PRODUCTION retriever is intentionally absent in this first pass.
|
|
21
|
-
* There is no clean in-process MemPalace call surface yet (the plugin is
|
|
22
|
-
* an MCP server, not a typed module), and shelling out per message from
|
|
23
|
-
* the dispatch hot path is explicitly forbidden. Until a deliberate
|
|
24
|
-
* bridge exists, deployments get `noopMemoryRetriever` — plumbing lands
|
|
25
|
-
* inert, prompts stay byte-identical.
|
|
26
|
-
* - Trust-aware injection (#373): when a real adapter lands, only items
|
|
27
|
-
* with `trustLevel` in `AUTO_INJECT_TRUST_LEVELS` may be auto-injected.
|
|
28
|
-
* `user_claim` / `group_chat` content stays pull-only, or a poisoned
|
|
29
|
-
* drawer becomes a persistent prompt injection in every session.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import type {
|
|
33
|
-
RetrievedMemory,
|
|
34
|
-
RetrievedMemoryTrustLevel,
|
|
35
|
-
} from "../agent-runtime/capabilities.js";
|
|
36
|
-
|
|
37
|
-
// ── Retriever contract ──────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
/** Context handed to a retriever for one chat turn. */
|
|
40
|
-
export type MemoryRetrievalContext = {
|
|
41
|
-
runKind: "chat";
|
|
42
|
-
chatId: string;
|
|
43
|
-
/** The raw incoming message text (retrievers trim/cap it themselves). */
|
|
44
|
-
text: string;
|
|
45
|
-
senderName: string;
|
|
46
|
-
isGroup?: boolean;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A memory retriever: small context in, bounded plain data out.
|
|
51
|
-
* `undefined` means "inject nothing" — the turn proceeds unchanged.
|
|
52
|
-
*/
|
|
53
|
-
export type MemoryRetriever = (
|
|
54
|
-
context: MemoryRetrievalContext,
|
|
55
|
-
) => Promise<RetrievedMemory | undefined>;
|
|
56
|
-
|
|
57
|
-
// ── Trust policy (#373) ─────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Trust levels eligible for automatic injection. Anything else (including an
|
|
61
|
-
* absent `trustLevel`) must be filtered by adapters before returning items.
|
|
62
|
-
*/
|
|
63
|
-
export const AUTO_INJECT_TRUST_LEVELS: ReadonlySet<RetrievedMemoryTrustLevel> =
|
|
64
|
-
new Set(["dylan_direct", "bot_inferred", "heartbeat_synthesis"]);
|
|
65
|
-
|
|
66
|
-
/** True when an item's trust level allows automatic injection. */
|
|
67
|
-
export function isAutoInjectTrusted(
|
|
68
|
-
trustLevel: RetrievedMemoryTrustLevel | undefined,
|
|
69
|
-
): boolean {
|
|
70
|
-
return trustLevel !== undefined && AUTO_INJECT_TRUST_LEVELS.has(trustLevel);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Enforce the #373 trust policy on a retrieval result: drop every item that
|
|
75
|
-
* is not explicitly auto-inject trusted. Adapters should call this as their
|
|
76
|
-
* last step, and the Weaver's `prefetchMemory` applies it again to whatever
|
|
77
|
-
* a retriever returns — a buggy adapter cannot leak low-trust items into
|
|
78
|
-
* the prompt.
|
|
79
|
-
*/
|
|
80
|
-
export function filterAutoInjectable(
|
|
81
|
-
memory: RetrievedMemory | undefined,
|
|
82
|
-
): RetrievedMemory | undefined {
|
|
83
|
-
if (!memory) return undefined;
|
|
84
|
-
const items = memory.items.filter((i) => isAutoInjectTrusted(i.trustLevel));
|
|
85
|
-
if (items.length === 0) return undefined;
|
|
86
|
-
return { ...memory, items };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Default (inert) retriever ───────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
/** The no-op retriever: Phase B plumbing present, injection disabled. */
|
|
92
|
-
export const noopMemoryRetriever: MemoryRetriever = async () => undefined;
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory prefetch (Phase B) — optional pre-retrieval of palace memory
|
|
3
|
-
* for live user messages, strictly fail-closed: a broken palace must
|
|
4
|
-
* never block chat delivery. The result is dynamic turn context passed
|
|
5
|
-
* through ChatRunParams; it never touches the frozen prompt.
|
|
6
|
-
*
|
|
7
|
-
* The #373 trust policy is enforced HERE, not just in adapters: whatever a
|
|
8
|
-
* retriever returns is passed through `filterAutoInjectable` before it can
|
|
9
|
-
* reach the prompt, so a buggy or future adapter cannot leak `user_claim` /
|
|
10
|
-
* `group_chat` items into auto-injection.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { RetrievedMemory } from "../agent-runtime/capabilities.js";
|
|
14
|
-
import type { MemoryRetriever } from "../memory/retrieval.js";
|
|
15
|
-
import { filterAutoInjectable } from "../memory/retrieval.js";
|
|
16
|
-
import { logDebug, logWarn } from "../../util/log.js";
|
|
17
|
-
|
|
18
|
-
export type PrefetchMemoryInput = {
|
|
19
|
-
chatId: string;
|
|
20
|
-
text: string;
|
|
21
|
-
senderName: string;
|
|
22
|
-
isGroup: boolean;
|
|
23
|
-
/** Request id for log correlation. */
|
|
24
|
-
reqId: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export async function prefetchMemory(
|
|
28
|
-
retrieve: MemoryRetriever,
|
|
29
|
-
input: PrefetchMemoryInput,
|
|
30
|
-
): Promise<RetrievedMemory | undefined> {
|
|
31
|
-
try {
|
|
32
|
-
const raw = await retrieve({
|
|
33
|
-
runKind: "chat",
|
|
34
|
-
chatId: input.chatId,
|
|
35
|
-
text: input.text,
|
|
36
|
-
senderName: input.senderName,
|
|
37
|
-
isGroup: input.isGroup,
|
|
38
|
-
});
|
|
39
|
-
const retrieved = filterAutoInjectable(raw);
|
|
40
|
-
const droppedCount =
|
|
41
|
-
(raw?.items.length ?? 0) - (retrieved?.items.length ?? 0);
|
|
42
|
-
if (droppedCount > 0) {
|
|
43
|
-
logWarn(
|
|
44
|
-
"dispatcher",
|
|
45
|
-
`[${input.reqId}] memory pre-retrieval: dropped ${droppedCount} ` +
|
|
46
|
-
`low-trust item(s) the retriever failed to filter (#373)`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (retrieved) {
|
|
50
|
-
logDebug(
|
|
51
|
-
"dispatcher",
|
|
52
|
-
`[${input.reqId}] memory pre-retrieval: ${retrieved.items.length} item(s), ` +
|
|
53
|
-
`${retrieved.items.reduce((n, i) => n + i.text.length, 0)} chars`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return retrieved;
|
|
57
|
-
} catch (err) {
|
|
58
|
-
logWarn(
|
|
59
|
-
"dispatcher",
|
|
60
|
-
`[${input.reqId}] memory pre-retrieval failed (running turn without it): ` +
|
|
61
|
-
(err instanceof Error ? err.message : String(err)),
|
|
62
|
-
);
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
}
|