talon-agent 5.30.0 → 5.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "5.30.0",
3
+ "version": "5.31.0",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "The Falconry",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,15 @@ never a substitute for `report_result`.
50
50
 
51
51
  {% if canSpawn %}You may spawn your own sub-agents with `spawn_agent` (current depth {{depth}}, cap {{maxDepth}}) when the work genuinely splits into independent pieces. You are then responsible for them: `wait_for_agent`, `send_to_agent`, `kill_agent`, and folding their reports into yours.{% else %}You are at the maximum sub-agent depth ({{maxDepth}}) — `spawn_agent` will be refused. Do this work yourself.{% endif %}
52
52
 
53
- ## Boundaries
53
+ {% if scratchDir %}## Scratch space
54
+
55
+ Your private temp directory is `{{scratchDir}}` — `TMPDIR` points there for
56
+ your shells and tools. Put scratch files, clones and build output there
57
+ rather than in shared `/tmp`, where other agents are working. It is deleted
58
+ when you finish successfully and kept if your run fails, so leave anything
59
+ worth keeping somewhere permanent.
60
+
61
+ {% endif %}## Boundaries
54
62
 
55
63
  - Do **not** message the user's chat directly unless the brief explicitly
56
64
  tells you to. Your report goes to the agent or chat that spawned you, and
@@ -91,6 +91,9 @@ export async function runOneShotAgent(
91
91
  systemPrompt,
92
92
  ...thinkingConfig,
93
93
  cwd: workspace,
94
+ // A sub-agent's private TMPDIR (and any other per-run vars), layered
95
+ // over the daemon's environment — the SDK replaces, not merges, `env`.
96
+ ...(params.env ? { env: { ...process.env, ...params.env } } : {}),
94
97
  permissionMode: "bypassPermissions" as const,
95
98
  allowDangerouslySkipPermissions: true,
96
99
  abortController,
@@ -40,8 +40,11 @@ export function buildAgentSystemPrompt(args: {
40
40
  parent: AgentParent;
41
41
  depth: number;
42
42
  maxDepth: number;
43
+ /** The agent's private TMPDIR, when one was created. */
44
+ scratchDir?: string;
43
45
  }): string {
44
46
  return loadSystemTemplate("agent-brief", {
47
+ scratchDir: args.scratchDir ?? "",
45
48
  agentId: args.agentId,
46
49
  label: args.label,
47
50
  parent: describeParent(args.parent),
@@ -86,6 +86,7 @@ import { startWatchdog, type WatchdogHandle } from "./watchdog.js";
86
86
  import * as agentsRepo from "../../storage/agents/repo.js";
87
87
  import type { PersistedAgent } from "../../storage/agents/repo.js";
88
88
  import { agentRegistry } from "./registry.js";
89
+ import { closeScratch, openScratch, scratchEnv } from "./scratch.js";
89
90
  import type {
90
91
  AgentCaps,
91
92
  AgentParent,
@@ -428,6 +429,7 @@ async function buildRunParams(
428
429
  abortController: AbortController,
429
430
  capture: { last: string },
430
431
  trail: RunTrail,
432
+ scratchDir: string | undefined,
431
433
  resume?: ResumePlan,
432
434
  ): Promise<OneShotAgentParams> {
433
435
  const writeLog = await openRunLog(
@@ -458,7 +460,9 @@ async function buildRunParams(
458
460
  parent: record.parent,
459
461
  depth: record.depth,
460
462
  maxDepth: capsHolder.caps.maxDepth,
463
+ ...(scratchDir ? { scratchDir } : {}),
461
464
  }),
465
+ ...(scratchDir ? { env: scratchEnv(scratchDir) } : {}),
462
466
  workspace: dirs.workspace,
463
467
  model,
464
468
  contextLabel: agentContextLabel(record.id),
@@ -623,6 +627,7 @@ async function runAgent(
623
627
 
624
628
  let settled: AgentRecord | null = null;
625
629
  try {
630
+ const scratchDir = await openScratch(id);
626
631
  const params = await buildRunParams(
627
632
  record,
628
633
  spec,
@@ -630,6 +635,7 @@ async function runAgent(
630
635
  abortController,
631
636
  capture,
632
637
  trail,
638
+ scratchDir,
633
639
  resume,
634
640
  );
635
641
  if (agentRegistry.isInterrupted(id)) {
@@ -708,6 +714,8 @@ async function runAgent(
708
714
  return;
709
715
  }
710
716
  if (!settled) return;
717
+ // Removed on success; kept on failure for whoever picks the work up.
718
+ await closeScratch(id, settled.state);
711
719
  log(
712
720
  "agents",
713
721
  `${id} "${settled.label}" → ${settled.state} ` +
@@ -802,6 +810,7 @@ async function settleRestored(
802
810
  ): Promise<void> {
803
811
  const settled = agentRegistry.settle(record.id, patch);
804
812
  if (!settled) return;
813
+ await closeScratch(record.id, settled.state);
805
814
  log(
806
815
  "agents",
807
816
  `${record.id} "${record.label}" → ${settled.state} (after restart)`,
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Scratch — a private temp directory per sub-agent.
3
+ *
4
+ * Every agent gets `/tmp/talon-agents/<id>/` (under `os.tmpdir()`), created
5
+ * when its run starts and exported as `TMPDIR` / `TMP` / `TEMP` to the
6
+ * backend run (`OneShotAgentParams.env`), so its shells and tools write
7
+ * their temporary files somewhere no other agent is writing. The directory
8
+ * is named in the agent's system prompt too, for backends that cannot take
9
+ * a per-run environment.
10
+ *
11
+ * Lifecycle: kept across a daemon restart (a resumed agent finds its files
12
+ * where it left them), removed when the agent settles `done`, and **kept**
13
+ * on any other terminal state so whoever picks up a failed, killed or
14
+ * timed-out run can inspect what it left behind.
15
+ */
16
+
17
+ import { mkdir, rm } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { logWarn } from "../../util/log.js";
21
+ import type { AgentState } from "./types.js";
22
+
23
+ /** Root of every agent's scratch dir. Overridable for tests. */
24
+ const root: { dir: string } = { dir: join(tmpdir(), "talon-agents") };
25
+
26
+ export function setScratchRootForTest(dir: string): void {
27
+ root.dir = dir;
28
+ }
29
+
30
+ /** Absolute path of an agent's scratch dir (whether or not it exists). */
31
+ export function agentScratchDir(agentId: string): string {
32
+ return join(root.dir, agentId);
33
+ }
34
+
35
+ /** The env a run with this scratch dir is given. */
36
+ export function scratchEnv(dir: string): Record<string, string> {
37
+ return { TMPDIR: dir, TMP: dir, TEMP: dir };
38
+ }
39
+
40
+ /**
41
+ * Create (or reuse, on resume) the agent's scratch dir. Returns its path,
42
+ * or `undefined` when it could not be created — a run is never refused for
43
+ * want of a temp dir; it just shares the system one.
44
+ */
45
+ export async function openScratch(
46
+ agentId: string,
47
+ ): Promise<string | undefined> {
48
+ const dir = agentScratchDir(agentId);
49
+ try {
50
+ await mkdir(dir, { recursive: true, mode: 0o700 });
51
+ return dir;
52
+ } catch (err) {
53
+ logWarn(
54
+ "agents",
55
+ `could not create scratch dir ${dir}: ${err instanceof Error ? err.message : String(err)}`,
56
+ );
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Clean up after a settled agent: removed on `done`, kept otherwise.
63
+ * Returns whether the directory was removed. Never throws.
64
+ */
65
+ export async function closeScratch(
66
+ agentId: string,
67
+ state: AgentState,
68
+ ): Promise<boolean> {
69
+ if (state !== "done") return false;
70
+ try {
71
+ await rm(agentScratchDir(agentId), { recursive: true, force: true });
72
+ return true;
73
+ } catch (err) {
74
+ logWarn(
75
+ "agents",
76
+ `could not remove scratch dir for ${agentId}: ${err instanceof Error ? err.message : String(err)}`,
77
+ );
78
+ return false;
79
+ }
80
+ }
package/src/core/types.ts CHANGED
@@ -187,6 +187,13 @@ export type OneShotAgentParams = {
187
187
  * `onAssistantText`: a throwing callback must never fail the run.
188
188
  */
189
189
  onSessionId?: (sessionId: string) => void;
190
+ /**
191
+ * Extra environment for the run's own subprocesses (shells, tools),
192
+ * layered over the daemon's. A sub-agent passes its private scratch dir
193
+ * as `TMPDIR` here. Honoured by the Claude SDK backend; backends whose
194
+ * subprocess environment is fixed per process ignore it.
195
+ */
196
+ env?: Readonly<Record<string, string>>;
190
197
  };
191
198
 
192
199
  /** How much cache telemetry a backend can surface in /status. */