@parallel-cli/parallel 0.4.9 → 0.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to Parallel are documented here.
4
4
 
5
+ ## 0.5.0 - 2026-06-25
6
+
7
+ ### 0.5.0 Added
8
+
9
+ - Added an automatically generated, versioned project context in `.parallel/project-context.json`, shared by every new agent in the same folder.
10
+ - Added targeted freshness tracking for files inspected by agents, including content hashes and stale-file warnings.
11
+ - Added visible project-memory indexing, deterministic fallback, token/cost accounting, `/memory`, and `/memory refresh`.
12
+ - Added restored-session summaries directly to new-agent bootstrap context instead of relying on historical notes.
13
+ - Added an agent performance diagnostician and deterministic simulator for model rounds, tool churn, shell micro-commands, repeated reads, hidden compactions, and context amplification.
14
+ - Added adaptive Quick, Standard, and Deep execution profiles with visible badges and `--quick`, `--standard`, and `--deep` overrides.
15
+ - Added a persistent incremental lexical/symbol index under `.parallel/index/` and task-oriented retrieval before the first model call.
16
+ - Added targeted line-range reads, bounded tool output artifacts, provider retry/cache telemetry, and runtime convergence budgets.
17
+
18
+ ### 0.5.0 Changed
19
+
20
+ - New agents now start from shared architecture, conventions, pitfalls, entry points, and recent work instead of treating the repository as unknown.
21
+ - Replaced generic “explore first” prompting with targeted verification of relevant, unknown, stale, or soon-to-be-modified files.
22
+ - Kept full conversations isolated per agent; `/restore` remains the explicit path for exact conversation continuity.
23
+ - Session snapshots now record inspected files and project-context metadata while remaining compatible with older snapshots.
24
+ - Agent telemetry now records provider wait time, hidden compaction time/calls, and peak prompt tokens.
25
+ - Quick and Standard agents now keep a bounded recent window plus a deterministic work ledger instead of repeatedly sending every raw tool result.
26
+ - Project-memory enrichment now runs in the background; startup no longer waits up to 20 seconds before useful work can begin.
27
+ - Ordinary inter-agent notes are batched for the next natural turn instead of aborting an in-flight model request.
28
+ - Included stable Settings and Wizard list navigation/windowing fixes.
29
+
30
+ ### 0.5.0 Fixed
31
+
32
+ - Fixed newly spawned agents ignoring all useful work, notes, and conclusions that existed before their creation.
33
+ - Fixed loaded-session summaries being added to the blackboard and then skipped by the next agent’s note cursor.
34
+ - Fixed repetitive generic “Explore the project” progress steps when a valid project map already exists.
35
+ - Fixed simple investigations inheriting the same 60-turn allowance as long-running plans.
36
+ - Fixed large command and inspection results remaining in every later prompt.
37
+
5
38
  ## 0.4.9 - 2026-06-24
6
39
 
7
40
  ### 0.4.9 Added
package/README.md CHANGED
@@ -32,6 +32,7 @@ Parallel lets several AI coding agents co-edit the same repository at the same t
32
32
  - Keep shell execution controlled with `ask`, `auto-safe`, or `yolo` approvals.
33
33
  - Get prompted for npm updates at startup, with an explicit skip path.
34
34
  - Save and restore project sessions.
35
+ - Reuse a persistent, automatically synthesized project map across new agents in the same folder.
35
36
  - Run headless multi-agent jobs for CI or scripts.
36
37
 
37
38
  ## Install
@@ -138,6 +139,14 @@ The reviewer is ask-only: it does not edit, does not gate the session globally,
138
139
 
139
140
  Task and plan agents maintain a small Cursor-style checklist with one active step at a time. The runtime also encourages batched inspection through `read_many` and `inspect_project` so agents avoid slow chains of tiny read-only shell commands.
140
141
 
142
+ Every agent also receives an execution profile:
143
+
144
+ - `quick`: targeted questions, diagnostics, and small changes; six model turns by default.
145
+ - `standard`: bounded multi-file work; sixteen model turns by default.
146
+ - `deep`: plans, migrations, and long-running refactors; up to the configured global limit.
147
+
148
+ Parallel selects the profile locally without spending a model call. Override it when needed with `--quick`, `--standard`, or `--deep`, for example `/task --quick fix the sound toggle`. A profile only escalates automatically when the agent discovers concrete cross-file complexity; repeated exploration does not earn more budget.
149
+
141
150
  Aliases:
142
151
 
143
152
  - `/a` -> `/ask`
@@ -340,6 +349,8 @@ If the update succeeds, restart Parallel to run the new version. Use `parallel -
340
349
  - `/diff`: live diff history.
341
350
  - `/cost`: token and cost breakdown.
342
351
  - `/status`: session model, approval mode, agents, and cost snapshot.
352
+ - `/memory`: show shared project-memory freshness, model, tokens, and cost.
353
+ - `/memory refresh`: force a visible regeneration of the shared project map.
343
354
  - `/skills`: available skills.
344
355
  - `/specialists`: available specialists.
345
356
  - `/save [name]`: save the current session.
@@ -347,12 +358,16 @@ If the update succeeds, restart Parallel to run the new version. Use `parallel -
347
358
  - `/session <n|latest>`: load a saved session snapshot. If active agents are running, use `/session <n|latest> --force` after saving/stopping what you need.
348
359
  - `/restore <agent>`: relaunch a restored agent by name, alias, or saved id when its conversation history is still available.
349
360
 
350
- Session memory has two layers:
361
+ Project and session memory have three distinct layers:
351
362
 
352
363
  - Live memory: active agents see statuses, notes, claims, work-map warnings, file activity, and recent diffs before every model action.
353
- - Durable memory: `/save` and autosave persist notes, claims, recent diff excerpts, file activity, work-map warnings, agent aliases, model/provider metadata, context usage, and conversation paths for restore.
364
+ - Project memory: `.parallel/project-context.json` stores a model-generated architecture map, entry points, conventions, pitfalls, file hashes, and recent completed work. It loads automatically for every new agent in the same folder.
365
+ - Local index: `.parallel/index/manifest.json` incrementally records text files, symbols, imports, hashes, and searchable terms. Before the first model call, Parallel uses it to rank the files relevant to the current task.
366
+ - Session/conversation memory: `/save` and autosave persist coordination state and per-agent conversation paths for explicit `/restore`.
354
367
 
355
- Restore is best effort and explicit. `/session` reloads coordination memory into the blackboard; `/restore <agent>` relaunches an agent only when the saved conversation file still exists. Restored agents keep their prior task, mode, model, specialist, and conversation when available.
368
+ Parallel prewarms project memory when a project opens, but the first agent never waits for an LLM-generated synthesis. It immediately uses the persisted map, deterministic fallback, and local task-oriented index while enrichment continues in the background. `/memory` reports both map and index freshness.
369
+
370
+ Agents trust the project map for orientation, but re-read files that are relevant, unknown, stale, or about to be modified. Full conversations are never copied into unrelated new agents. Restore remains best effort and explicit: `/session` reloads coordination memory, while `/restore <agent>` relaunches the selected agent with its prior conversation when available.
356
371
 
357
372
  ### Settings And Exit
358
373
 
@@ -365,7 +380,7 @@ Restore is best effort and explicit. `/session` reloads coordination memory into
365
380
  - `/folder [folder]`: alias for `/project`.
366
381
  - `/wizard`: relaunch the setup wizard. If agents are active, use `/wizard --force` after saving/stopping what you need.
367
382
  - `/setup`: alias for `/wizard`.
368
- - `/doctor`: run local readiness diagnostics for provider, key, model, endpoint, attach socket, and Git tooling.
383
+ - `/doctor`: run local readiness diagnostics for provider, key, model, endpoint, project memory, attach socket, and Git tooling.
369
384
  - `/help`: full command reference.
370
385
  - `/quit`: save the session and exit.
371
386
 
@@ -392,7 +407,7 @@ Parallel separates agent modes from shell approval behavior.
392
407
  Parallel stores credentials and session state with owner-only permissions where supported:
393
408
 
394
409
  - `~/.parallel/config.json` and `~/.parallel/update.json` are written privately and atomically.
395
- - Project runtime files under `.parallel/` use private directories for sessions, conversations, memory, socket state, and attach tokens.
410
+ - Project runtime files under `.parallel/` use private directories for sessions, conversations, project context, memory, socket state, and attach tokens.
396
411
  - Attached terminals authenticate to the running session with a per-session token; local clients without the token cannot steer agents or answer approvals.
397
412
  - `/doctor` reports local permission warnings alongside provider, model, endpoint, attach socket, `git`, and `gh` checks.
398
413
  - Command output shown in logs is sanitized to strip terminal escape/control sequences.
@@ -402,7 +417,9 @@ Shell safety is still a shared responsibility. `auto-safe` uses conservative heu
402
417
 
403
418
  ## Sessions, Skills, And Specialists
404
419
 
405
- Parallel stores project state under `.parallel/` in the selected project directory. That includes saved sessions, memory, skills, specialists, and session socket state.
420
+ Parallel stores project state under `.parallel/` in the selected project directory. That includes saved sessions, the generated project context, durable facts, skills, specialists, and session socket state.
421
+
422
+ `.parallel/state.json` remains a best-effort diagnostic snapshot. It is not loaded as conversation history; use project memory for shared understanding and `/restore` for exact agent continuity.
406
423
 
407
424
  Skills are markdown instruction files agents can load with the `load_skill` tool or that you can force-load with `#skill-name` in a task:
408
425
 
@@ -1,12 +1,14 @@
1
+ import path from 'node:path';
1
2
  import * as Diff from 'diff';
2
3
  import { ToolExecutor, TOOL_DEFINITIONS } from './tools.js';
3
4
  import { costOf } from '../pricing.js';
4
5
  import { skillsCatalog } from '../skills.js';
5
6
  import { getLang, LANG_NAME_EN, t } from '../i18n.js';
6
- import { appendFilePrivate, sanitizeForPersistence } from '../security.js';
7
+ import { appendFilePrivate, sanitizeForPersistence, writeFileAtomicPrivate } from '../security.js';
8
+ import { EXECUTION_BUDGETS, nextExecutionProfile, shouldEscalateExecution, } from './execution-policy.js';
7
9
  // Agent-facing prompts stay in English (canonical for models). Only notes
8
10
  // addressed to the user follow the configured UI language.
9
- const SYSTEM_PROMPT = (name, task, mode, userLang, skillsList, specialist, projectMemory) => `You are agent "${name}", an autonomous software engineer inside PARALLEL, an environment where SEVERAL agents work at the same time on the SAME project, each on its own task given by the user.
11
+ const SYSTEM_PROMPT = (name, task, mode, userLang, skillsList, specialist, projectMemory, projectContext, profile = 'standard') => `You are agent "${name}", an autonomous software engineer inside PARALLEL, an environment where SEVERAL agents work at the same time on the SAME project, each on its own task given by the user.
10
12
  ${specialist
11
13
  ? `
12
14
  YOUR ROLE — you are the "${specialist.name}" specialist:
@@ -19,6 +21,19 @@ ${task}
19
21
  </user_task>
20
22
 
21
23
  AGENT MODE: ${mode}
24
+ EXECUTION PROFILE: ${profile}
25
+ ${profile === 'quick'
26
+ ? `QUICK PROFILE:
27
+ - This task must converge in a few model turns.
28
+ - Do not create a progress checklist unless the task unexpectedly becomes multi-file.
29
+ - Use the task-oriented local index first, batch the smallest relevant inspection, then conclude.
30
+ - Do not spend a turn only updating status or steps.`
31
+ : profile === 'standard'
32
+ ? `STANDARD PROFILE:
33
+ - Keep inspection bounded and use a checklist only when there are multiple distinct outcomes.
34
+ - Escalation is justified by discovered cross-file complexity, not by repeated exploration.`
35
+ : `DEEP PROFILE:
36
+ - Multi-step planning and broader validation are allowed, but every turn must make concrete progress.`}
22
37
  ${mode === 'ask'
23
38
  ? `ASK MODE:
24
39
  - You are advisory only. Do not modify files.
@@ -28,8 +43,8 @@ ${mode === 'ask'
28
43
  - Finish with task_complete using this user-facing structure in ${userLang}: "Réponse courte", "Recommandation", "Pourquoi", "Prochaines étapes".`
29
44
  : mode === 'plan'
30
45
  ? `PLAN MODE:
31
- - Explore first with read-only tools.
32
- - Batch independent reads/searches with read_many or inspect_project. Keep exploration broad enough to be correct but bounded.
46
+ - Start from the shared project context. Inspect only the task-relevant files that are unknown, stale, or needed as evidence.
47
+ - Batch independent targeted reads/searches with read_many or inspect_project.
33
48
  - Before modifying any file or running mutating commands, call ask_user with a concrete implementation plan.
34
49
  - The plan must include steps, files you expect to touch, risks, and validation.
35
50
  - Use options ["Approve", "Revise"], recommended "Revise" so timeout never approves changes.
@@ -37,7 +52,7 @@ ${mode === 'ask'
37
52
  - Finish with task_complete using this user-facing structure in ${userLang}: "Plan appliqué", "Ce que j’ai modifié", "Validation", "Risques restants".`
38
53
  : `TASK MODE:
39
54
  - Execute the user's objective end-to-end.
40
- - Use this loop: create visible steps, batch inspect, act, batch validate, summarize.
55
+ - Use this loop: create outcome-oriented visible steps, verify the relevant context, act, batch validate, summarize.
41
56
  - If the task is a verification/audit and the correct outcome is no file changes, that is valid task work. Say explicitly in task_complete that no modification was necessary and why.
42
57
  - Ask the user only when blocked or when a risky product decision cannot be inferred.
43
58
  - Finish with task_complete using this user-facing structure in ${userLang}: "Ce que j’ai fait", "Ce que j’ai vérifié", "Résultat", "Détails techniques".`}
@@ -53,6 +68,13 @@ PROJECT MEMORY — durable facts recorded by previous agents on this project. Tr
53
68
  <project_memory>
54
69
  ${projectMemory}
55
70
  </project_memory>
71
+ `
72
+ : ''}${projectContext
73
+ ? `
74
+ SHARED PROJECT CONTEXT — automatically maintained across agents in this folder:
75
+ <project_context>
76
+ ${projectContext}
77
+ </project_context>
56
78
  `
57
79
  : ''}
58
80
 
@@ -73,7 +95,9 @@ PARALLEL'S PHILOSOPHY — REAL-TIME CO-EDITING, NEVER ANY BLOCKING:
73
95
 
74
96
  WORK METHOD:
75
97
  - For non-trivial work, call update_steps early with 3-6 concrete steps. Keep exactly one step active and mark steps done as you complete them.
76
- - Explore first before modifying. Decide all independent reads/searches you need, then batch them with read_many or inspect_project instead of calling tools one by one.
98
+ - Do not create a generic "explore the project" step when shared project context already describes the codebase. Steps must state task-specific outcomes.
99
+ - Use shared project context first. Re-read only files directly relevant to the task, files marked stale/unknown, and every file immediately before modifying it.
100
+ - If the shared context is absent or insufficient for the task area, perform a bounded inspection and record durable discoveries.
77
101
  - Use run_command for builds/tests/validation and genuinely useful shell scripts. Do NOT spend many turns running grep/head/tail/wc/awk cascades; batch independent shell checks into one labelled command or use inspect_project.
78
102
  - Declare your work area with claim_files when you start (and when it changes): it prevents collisions without ever locking anything.
79
103
  - If you discover a durable, non-obvious fact about the project (convention, decision, pitfall), save it with remember(fact) for future agents.
@@ -98,6 +122,12 @@ const EMPTY_PERF = {
98
122
  shellCommands: 0,
99
123
  shellMs: 0,
100
124
  readOnlyShellCommands: 0,
125
+ llmMs: 0,
126
+ compactionTurns: 0,
127
+ compactionMs: 0,
128
+ maxPromptTokens: 0,
129
+ retries: 0,
130
+ cachedTokens: 0,
101
131
  };
102
132
  function noChangeTaskLine() {
103
133
  switch (getLang()) {
@@ -124,20 +154,26 @@ export class Agent {
124
154
  llm;
125
155
  board;
126
156
  maxSteps;
157
+ budget;
127
158
  abort = new AbortController();
128
159
  paused = false;
129
160
  stopped = false;
130
161
  lastNoteId = 0;
131
162
  lastChangeId = 0;
132
163
  readOnlyShellStreak = 0;
164
+ artifactSeq = 0;
165
+ convergenceWarned = new Set();
133
166
  constructor(opts) {
134
167
  this.opts = opts;
135
168
  this.id = opts.id;
136
169
  this.name = opts.name;
137
170
  this.llm = opts.llm;
138
171
  this.board = opts.board;
139
- this.maxSteps = opts.maxSteps;
140
- this.executor = new ToolExecutor(opts.board, opts.id, opts.name, opts.projectRoot, opts.requestApproval, opts.requestQuestion, opts.skills, opts.mode);
172
+ const profile = opts.profile ?? (opts.mode === 'plan' ? 'deep' : opts.mode === 'ask' ? 'quick' : 'standard');
173
+ const budget = opts.budget ?? EXECUTION_BUDGETS[profile];
174
+ this.maxSteps = Math.min(opts.maxSteps, budget.maxRounds);
175
+ this.budget = budget;
176
+ this.executor = new ToolExecutor(opts.board, opts.id, opts.name, opts.projectRoot, opts.requestApproval, opts.requestQuestion, opts.skills, opts.mode, opts.onInspect, profile, budget.maxResultChars);
141
177
  const info = {
142
178
  id: opts.id,
143
179
  name: opts.name,
@@ -145,6 +181,7 @@ export class Agent {
145
181
  color: opts.color,
146
182
  task: opts.task,
147
183
  mode: opts.mode,
184
+ profile,
148
185
  model: opts.model,
149
186
  state: 'idle',
150
187
  currentAction: '',
@@ -273,13 +310,66 @@ export class Agent {
273
310
  const current = this.board.agents.get(this.id)?.perf ?? EMPTY_PERF;
274
311
  this.board.updateAgent(this.id, {
275
312
  perf: {
276
- modelTurns: current.modelTurns + (delta.modelTurns ?? 0),
277
- toolCalls: current.toolCalls + (delta.toolCalls ?? 0),
278
- shellCommands: current.shellCommands + (delta.shellCommands ?? 0),
279
- shellMs: current.shellMs + (delta.shellMs ?? 0),
280
- readOnlyShellCommands: current.readOnlyShellCommands + (delta.readOnlyShellCommands ?? 0),
313
+ modelTurns: (current.modelTurns ?? 0) + (delta.modelTurns ?? 0),
314
+ toolCalls: (current.toolCalls ?? 0) + (delta.toolCalls ?? 0),
315
+ shellCommands: (current.shellCommands ?? 0) + (delta.shellCommands ?? 0),
316
+ shellMs: (current.shellMs ?? 0) + (delta.shellMs ?? 0),
317
+ readOnlyShellCommands: (current.readOnlyShellCommands ?? 0) + (delta.readOnlyShellCommands ?? 0),
318
+ llmMs: (current.llmMs ?? 0) + (delta.llmMs ?? 0),
319
+ compactionTurns: (current.compactionTurns ?? 0) + (delta.compactionTurns ?? 0),
320
+ compactionMs: (current.compactionMs ?? 0) + (delta.compactionMs ?? 0),
321
+ maxPromptTokens: Math.max(current.maxPromptTokens ?? 0, delta.maxPromptTokens ?? 0),
322
+ retries: (current.retries ?? 0) + (delta.retries ?? 0),
323
+ cachedTokens: (current.cachedTokens ?? 0) + (delta.cachedTokens ?? 0),
324
+ },
325
+ });
326
+ }
327
+ boundedHistory() {
328
+ const limit = this.budget.maxRecentMessages;
329
+ if (this.history.length <= limit)
330
+ return this.history;
331
+ let cut = Math.max(1, this.history.length - limit);
332
+ while (cut < this.history.length && this.history[cut].role === 'tool')
333
+ cut++;
334
+ const removed = this.history.slice(1, cut);
335
+ const actions = [];
336
+ for (const message of removed) {
337
+ if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
338
+ for (const call of message.tool_calls) {
339
+ actions.push(`${call.function?.name ?? 'tool'}(${String(call.function?.arguments ?? '').slice(0, 100)})`);
340
+ }
341
+ }
342
+ else if (message.role === 'tool') {
343
+ actions.push(`result: ${String(message.content ?? '').replace(/\s+/g, ' ').slice(0, 140)}`);
344
+ }
345
+ if (actions.length >= 24)
346
+ break;
347
+ }
348
+ return [
349
+ this.history[0],
350
+ {
351
+ role: 'user',
352
+ content: `[DETERMINISTIC WORK LEDGER — older raw outputs omitted]\n${actions.map((item) => `- ${item}`).join('\n') || '- Earlier context omitted.'}`,
281
353
  },
354
+ ...this.history.slice(cut),
355
+ ];
356
+ }
357
+ maybeEscalate() {
358
+ const next = nextExecutionProfile(this.budget.profile);
359
+ if (!next)
360
+ return false;
361
+ const info = this.board.agents.get(this.id);
362
+ const changedFiles = new Set(this.board.changes.filter((change) => change.agentId === this.id).map((change) => change.path)).size;
363
+ if (!shouldEscalateExecution(this.opts.task, info?.inspectedFiles?.length ?? 0, changedFiles))
364
+ return false;
365
+ this.budget = EXECUTION_BUDGETS[next];
366
+ this.maxSteps = Math.min(this.opts.maxSteps, this.budget.maxRounds);
367
+ this.board.updateAgent(this.id, { profile: next, currentAction: `budget escalated to ${next}` });
368
+ this.record({
369
+ role: 'user',
370
+ content: `[EXECUTION PROFILE ESCALATED TO ${next.toUpperCase()}] Concrete task complexity justified more budget. Continue with targeted work; repeated exploration is not justification for another escalation.`,
282
371
  });
372
+ return true;
283
373
  }
284
374
  /**
285
375
  * Build the live context injected before EVERY model call:
@@ -287,6 +377,9 @@ export class Agent {
287
377
  * Returns { text, hasNews } — hasNews drives the 'listening' state.
288
378
  */
289
379
  liveContext() {
380
+ if (this.board.agents.size <= 1) {
381
+ return { text: '[REAL TIME] No other active agent context. Continue with the smallest useful next action.', hasNews: false };
382
+ }
290
383
  let hasNews = false;
291
384
  const parts = ['[REAL TIME]', this.board.snapshotFor(this.id)];
292
385
  const notes = this.board.notesFor(this.name, this.lastNoteId);
@@ -324,6 +417,14 @@ export class Agent {
324
417
  return { text: parts.join('\n'), hasNews };
325
418
  }
326
419
  async run() {
420
+ this.board.setAgentState(this.id, 'working', 'loading project memory');
421
+ let sharedProjectContext = '';
422
+ try {
423
+ sharedProjectContext = (await this.opts.projectContext) ?? '';
424
+ }
425
+ catch {
426
+ sharedProjectContext = '';
427
+ }
327
428
  this.board.setAgentState(this.id, 'working', 'starting');
328
429
  if (this.opts.initialHistory && this.opts.initialHistory.length > 0) {
329
430
  // Resume a previous conversation (/restore): re-record everything into
@@ -333,13 +434,15 @@ export class Agent {
333
434
  this.record(m);
334
435
  this.record({
335
436
  role: 'user',
336
- content: '[SESSION RESTORED] This conversation was saved and has just been restored. Time has passed: files may have changed on disk. Re-read the files you rely on before editing them, then continue your task from where you left off.',
437
+ content: `[SESSION RESTORED] This conversation was saved and has just been restored. Continue from where you left off. Use the shared project context below to identify what changed, and re-read only task-relevant files marked stale or files you are about to modify.
438
+
439
+ ${sharedProjectContext}`,
337
440
  });
338
441
  }
339
442
  else {
340
443
  this.record({
341
444
  role: 'system',
342
- content: SYSTEM_PROMPT(this.name, this.opts.task, this.opts.mode, LANG_NAME_EN[getLang()], skillsCatalog(this.opts.skills), this.opts.specialist, this.opts.projectMemory),
445
+ content: SYSTEM_PROMPT(this.name, this.opts.task, this.opts.mode, LANG_NAME_EN[getLang()], skillsCatalog(this.opts.skills), this.opts.specialist, this.opts.projectMemory, sharedProjectContext, this.budget.profile),
343
446
  });
344
447
  // Pasted images (multimodal models): attached to the very first user turn.
345
448
  if (this.opts.images && this.opts.images.length > 0) {
@@ -360,9 +463,24 @@ export class Agent {
360
463
  */
361
464
  async loop() {
362
465
  let steps = 0;
466
+ let closingTurnGranted = false;
363
467
  try {
364
468
  this.finished = false;
365
- while (!this.stopped && steps < this.maxSteps) {
469
+ while (!this.stopped) {
470
+ if (steps >= this.maxSteps) {
471
+ if (this.maybeEscalate())
472
+ continue;
473
+ if (!closingTurnGranted) {
474
+ closingTurnGranted = true;
475
+ this.maxSteps++;
476
+ this.record({
477
+ role: 'user',
478
+ content: '[FINAL BUDGET TURN] Do not inspect further. Call task_complete now with the strongest conclusion supported by current evidence, explicitly stating any remaining uncertainty.',
479
+ });
480
+ continue;
481
+ }
482
+ break;
483
+ }
366
484
  await this.waitWhilePaused();
367
485
  if (this.stopped)
368
486
  break;
@@ -380,13 +498,12 @@ export class Agent {
380
498
  if (live.hasNews) {
381
499
  // Visible (and audible via state event) cue: the agent is listening to the others.
382
500
  this.board.setAgentState(this.id, 'listening', 'reading the other agents’ work…');
383
- await new Promise((r) => setTimeout(r, 600));
384
501
  if (this.stopped)
385
502
  break;
386
503
  }
387
504
  this.repairToolCallHistory();
388
505
  const messages = [
389
- ...this.history,
506
+ ...this.boundedHistory(),
390
507
  { role: 'user', content: live.text },
391
508
  ];
392
509
  this.board.setAgentState(this.id, 'thinking');
@@ -396,8 +513,12 @@ export class Agent {
396
513
  const onStop = () => this.llmAbort?.abort();
397
514
  this.abort.signal.addEventListener('abort', onStop, { once: true });
398
515
  let res;
516
+ const llmStartedAt = Date.now();
399
517
  try {
400
- res = await this.llm.chat(messages, TOOL_DEFINITIONS, this.llmAbort.signal);
518
+ res = await this.llm.chat(messages, TOOL_DEFINITIONS, this.llmAbort.signal, {
519
+ maxTokens: this.budget.profile === 'quick' ? 2_048 : 4_096,
520
+ timeoutMs: this.budget.profile === 'quick' ? 45_000 : this.budget.profile === 'standard' ? 90_000 : 180_000,
521
+ });
401
522
  }
402
523
  catch (err) {
403
524
  if (!this.stopped && this.steered) {
@@ -415,7 +536,13 @@ export class Agent {
415
536
  this.llmAbort = null;
416
537
  }
417
538
  this.steered = false;
418
- this.updatePerf({ modelTurns: 1 });
539
+ this.updatePerf({
540
+ modelTurns: 1,
541
+ llmMs: Date.now() - llmStartedAt,
542
+ maxPromptTokens: res.tokensIn,
543
+ retries: res.retries,
544
+ cachedTokens: res.cachedTokens,
545
+ });
419
546
  const a = this.board.agents.get(this.id);
420
547
  if (a) {
421
548
  // Real-time financial view: accrue the cost of this round immediately.
@@ -429,6 +556,15 @@ export class Agent {
429
556
  ctxPct: Math.min(100, Math.round((res.tokensIn / CONTEXT_WINDOW) * 100)),
430
557
  });
431
558
  }
559
+ const currentPerf = this.board.agents.get(this.id)?.perf;
560
+ const budgetRatio = Math.max(steps / this.budget.maxRounds, (this.board.agents.get(this.id)?.tokensIn ?? 0) / this.budget.maxInputTokens, (currentPerf?.toolCalls ?? 0) / this.budget.maxToolCalls);
561
+ if (budgetRatio >= this.budget.convergenceAt && !this.convergenceWarned.has(this.budget.profile)) {
562
+ this.convergenceWarned.add(this.budget.profile);
563
+ this.record({
564
+ role: 'user',
565
+ content: '[BUDGET CONVERGENCE] You are approaching this execution profile budget. Stop broad exploration. Use the evidence already collected, perform at most one targeted verification, then call task_complete.',
566
+ });
567
+ }
432
568
  const msg = res.message;
433
569
  if (msg.content && msg.content.trim()) {
434
570
  // "✻" marks thinking/commentary steps — visually distinct from tool lines.
@@ -486,11 +622,34 @@ export class Agent {
486
622
  this.board.updateAgent(this.id, { currentAction: label.slice(0, 80) });
487
623
  const shellStartedAt = tc.function.name === 'run_command' ? Date.now() : 0;
488
624
  let result;
489
- try {
490
- result = await this.executor.execute(tc.function.name, args);
625
+ const perfBefore = this.board.agents.get(this.id)?.perf;
626
+ if ((perfBefore?.toolCalls ?? 0) >= this.budget.maxToolCalls) {
627
+ result = 'BUDGET: tool-call limit reached. Conclude with the evidence already collected.';
628
+ }
629
+ else if (tc.function.name === 'run_command' && (perfBefore?.shellCommands ?? 0) >= this.budget.maxShellCommands) {
630
+ result = 'BUDGET: shell-command limit reached. Use existing evidence or a non-shell targeted tool, then conclude.';
631
+ }
632
+ else {
633
+ try {
634
+ result = await this.executor.execute(tc.function.name, args);
635
+ }
636
+ catch (err) {
637
+ result = `ERROR: ${err?.message ?? String(err)}`;
638
+ }
491
639
  }
492
- catch (err) {
493
- result = `ERROR: ${err?.message ?? String(err)}`;
640
+ if (result.length > this.budget.maxResultChars) {
641
+ const artifactId = `artifact-${++this.artifactSeq}.txt`;
642
+ const artifactFile = path.join(this.opts.projectRoot, '.parallel', 'runs', this.id, 'artifacts', artifactId);
643
+ try {
644
+ writeFileAtomicPrivate(artifactFile, result);
645
+ result =
646
+ `${result.slice(0, this.budget.maxResultChars)}\n` +
647
+ `... (${result.length.toLocaleString()} characters total; full output stored as ${artifactId}. ` +
648
+ `Use read_artifact with this id and a targeted line range if more evidence is required.)`;
649
+ }
650
+ catch {
651
+ result = `${result.slice(0, this.budget.maxResultChars)}\n... (truncated by execution budget)`;
652
+ }
494
653
  }
495
654
  const shellMs = shellStartedAt ? Date.now() - shellStartedAt : 0;
496
655
  const readOnlyShell = tc.function.name === 'run_command' && isReadOnlyShell(String(args.command ?? ''));
@@ -524,6 +683,7 @@ export class Agent {
524
683
  lastResult: `${noChangePrefix}${summary}`,
525
684
  progressSteps: (this.board.agents.get(this.id)?.progressSteps ?? []).map((s) => ({ ...s, status: 'done' })),
526
685
  });
686
+ this.opts.onComplete?.(this.id, summary);
527
687
  // ONE short headline note (the full summary lives in lastResult and
528
688
  // is rendered as the agent's recap) — no duplicated walls of text.
529
689
  const headline = summary.split('\n').find((l) => l.trim())?.trim() ?? 'Task complete.';
@@ -547,7 +707,8 @@ export class Agent {
547
707
  this.board.setAgentState(this.id, 'done', 'done ✅');
548
708
  return;
549
709
  }
550
- await this.compactHistory();
710
+ if (this.budget.profile === 'deep')
711
+ await this.compactHistory();
551
712
  }
552
713
  if (!this.stopped) {
553
714
  this.board.setAgentState(this.id, 'error', `step limit of ${this.maxSteps} reached`);
@@ -572,6 +733,8 @@ export class Agent {
572
733
  return `📖 read ${args.path}`;
573
734
  case 'read_many':
574
735
  return `📚 read ${Array.isArray(args.paths) ? args.paths.slice(0, 3).join(', ') : 'files'}`;
736
+ case 'read_artifact':
737
+ return `📖 artifact ${args.id}`;
575
738
  case 'write_file':
576
739
  return `✏ write ${args.path}`;
577
740
  case 'edit_file':
@@ -662,6 +825,7 @@ export class Agent {
662
825
  }
663
826
  this.board.updateAgent(this.id, { currentAction: t('agent.compactingShort') });
664
827
  this.board.log(this.id, 'memory', t('agent.compactingStart'));
828
+ const compactStartedAt = Date.now();
665
829
  const res = await this.llm.chat([
666
830
  {
667
831
  role: 'system',
@@ -669,6 +833,11 @@ export class Agent {
669
833
  },
670
834
  { role: 'user', content: lines.join('\n') },
671
835
  ], undefined, this.abort.signal);
836
+ this.updatePerf({
837
+ compactionTurns: 1,
838
+ compactionMs: Date.now() - compactStartedAt,
839
+ maxPromptTokens: res.tokensIn,
840
+ });
672
841
  const a = this.board.agents.get(this.id);
673
842
  if (a) {
674
843
  const price = this.opts.price;
@@ -0,0 +1,58 @@
1
+ export const EXECUTION_BUDGETS = {
2
+ quick: {
3
+ profile: 'quick',
4
+ maxRounds: 6,
5
+ maxToolCalls: 12,
6
+ maxShellCommands: 2,
7
+ maxInputTokens: 150_000,
8
+ maxResultChars: 8_000,
9
+ maxRecentMessages: 24,
10
+ convergenceAt: 0.7,
11
+ },
12
+ standard: {
13
+ profile: 'standard',
14
+ maxRounds: 16,
15
+ maxToolCalls: 32,
16
+ maxShellCommands: 6,
17
+ maxInputTokens: 600_000,
18
+ maxResultChars: 16_000,
19
+ maxRecentMessages: 42,
20
+ convergenceAt: 0.75,
21
+ },
22
+ deep: {
23
+ profile: 'deep',
24
+ maxRounds: 60,
25
+ maxToolCalls: 120,
26
+ maxShellCommands: 30,
27
+ maxInputTokens: 3_000_000,
28
+ maxResultChars: 32_000,
29
+ maxRecentMessages: 80,
30
+ convergenceAt: 0.82,
31
+ },
32
+ };
33
+ const COMPLEX = /\b(migrat|refactor|architecture|redesign|rewrite|exhaustive|end[- ]to[- ]end|across|monorepo|multi[- ]service|security audit|performance audit|release|deploy)\b/i;
34
+ const SIMPLE = /\b(explain|find|locate|where|why|diagnos|inspect|verify|check|typo|rename|toggle|small|simple)\b/i;
35
+ export function classifyExecutionProfile(task, mode, forced) {
36
+ if (forced)
37
+ return forced;
38
+ if (mode === 'plan')
39
+ return 'deep';
40
+ if (mode === 'ask')
41
+ return COMPLEX.test(task) || task.length > 1_200 ? 'standard' : 'quick';
42
+ const pathMentions = task.match(/\b[\w./-]+\.(?:ts|tsx|js|mjs|json|md|py|rs|go|java)\b/g)?.length ?? 0;
43
+ if (COMPLEX.test(task) || pathMentions > 3 || task.length > 1_600)
44
+ return 'standard';
45
+ if (SIMPLE.test(task) || pathMentions <= 1 || task.length < 500)
46
+ return 'quick';
47
+ return 'standard';
48
+ }
49
+ export function nextExecutionProfile(profile) {
50
+ if (profile === 'quick')
51
+ return 'standard';
52
+ if (profile === 'standard')
53
+ return 'deep';
54
+ return null;
55
+ }
56
+ export function shouldEscalateExecution(task, inspectedFiles, changedFiles) {
57
+ return COMPLEX.test(task) || inspectedFiles > 3 || changedFiles > 3;
58
+ }