@bermudi/pi-delegate 0.1.10 → 0.1.12
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 +57 -4
- package/concurrency.ts +55 -16
- package/config.ts +584 -50
- package/delegate.ts +11 -3
- package/dispatch.ts +436 -65
- package/extension.ts +16 -4
- package/format.ts +52 -6
- package/host-cache.ts +70 -0
- package/host.ts +169 -811
- package/isolated-workspace.ts +857 -0
- package/lifecycle.ts +699 -520
- package/manual.ts +4 -7
- package/package.json +1 -1
- package/pi-package-source.ts +293 -0
- package/pool.ts +23 -1
- package/provider-extensions.ts +537 -0
- package/quiescence.ts +262 -0
- package/render-branches.ts +30 -0
- package/render-result.ts +8 -5
- package/runner.ts +35 -141
- package/schema.ts +133 -202
- package/settings.ts +202 -84
- package/shared-write-safety.ts +273 -0
- package/task-resolution.ts +121 -32
- package/telemetry.ts +135 -68
- package/ticket-format.ts +331 -0
- package/tickets.ts +101 -258
- package/tools.ts +12 -0
- package/trusted-paths.ts +71 -0
- package/types.ts +83 -16
package/extension.ts
CHANGED
|
@@ -18,12 +18,13 @@ import {
|
|
|
18
18
|
} from "./dispatch.ts";
|
|
19
19
|
import { renderDelegateCall, renderDelegateResult } from "./render-result.ts";
|
|
20
20
|
import { hostCompatError } from "./host-compat.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
registerProviderExtensionNotifier,
|
|
24
|
-
} from "./host.ts";
|
|
21
|
+
import { invalidateHostDepsCache } from "./host.ts";
|
|
22
|
+
import { registerProviderExtensionNotifier } from "./provider-extensions.ts";
|
|
25
23
|
import { recordTreeNavigation, resetLeafTracking } from "./leaf.ts";
|
|
26
24
|
import { closeAllPooledAgents } from "./pool.ts";
|
|
25
|
+
import { reconfigureGlobalConcurrency } from "./concurrency.ts";
|
|
26
|
+
import { reloadDelegateConfig, getMaxConcurrent } from "./config.ts";
|
|
27
|
+
import { warnLegacyDelegateSettingsMoved } from "./settings.ts";
|
|
27
28
|
import {
|
|
28
29
|
activeTicketSummary,
|
|
29
30
|
clearDelegateStatusContext,
|
|
@@ -125,6 +126,17 @@ export default function delegateExtension(pi: ExtensionAPI): void {
|
|
|
125
126
|
prepareArguments: normalizeDelegateArguments,
|
|
126
127
|
|
|
127
128
|
async execute(_id, params: DelegateArguments, signal, onUpdate, ctx) {
|
|
129
|
+
// Reload user-edited delegate.json at the start of every execution.
|
|
130
|
+
// Help, poll, cancel, wait, and invalid calls observe new settings, and
|
|
131
|
+
// the global concurrency cap is reconfigured so hot-reloaded maxConcurrent
|
|
132
|
+
// takes effect for subsequent acquisitions. A parse/read error keeps the
|
|
133
|
+
// previous snapshot and warns instead of falling back to defaults.
|
|
134
|
+
warnLegacyDelegateSettingsMoved(ctx.cwd, (message) =>
|
|
135
|
+
ctx.ui.notify(message, "warning"),
|
|
136
|
+
);
|
|
137
|
+
reloadDelegateConfig();
|
|
138
|
+
reconfigureGlobalConcurrency(getMaxConcurrent());
|
|
139
|
+
|
|
128
140
|
// Prime the UI notice for best-effort provider extensions that load for
|
|
129
141
|
// subagents (host.ts consumes it where the fact is discovered). Every
|
|
130
142
|
// execute re-primes so a stale ctx never sticks.
|
package/format.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { renderOutputForLLM } from "./spill.ts";
|
|
5
|
+
import { getOutputSpillThreshold, getOutputSpillTail } from "./config.ts";
|
|
5
6
|
import type {
|
|
6
7
|
ResolvedTask,
|
|
7
8
|
TaskProgress,
|
|
@@ -355,7 +356,13 @@ function isResumableSessionFile(sessionFile: string): boolean {
|
|
|
355
356
|
* notice instead of a retry hint — so the parent model is told to re-dispatch
|
|
356
357
|
* fresh rather than left to fabricate a path or chase a dead resume.
|
|
357
358
|
*/
|
|
358
|
-
export function formatFailedTask(
|
|
359
|
+
export function formatFailedTask(
|
|
360
|
+
r: TaskResult,
|
|
361
|
+
cwd?: string,
|
|
362
|
+
/** Dispatch-scoped snapshot for output-spill bounds; falls back to live
|
|
363
|
+
* config when unset (direct callers, legacy tests). */
|
|
364
|
+
config?: import("./config.ts").DelegateConfig,
|
|
365
|
+
): string[] {
|
|
359
366
|
const parts: string[] = [];
|
|
360
367
|
const isAbort = r.error === "Aborted";
|
|
361
368
|
// Empty string is falsy but not nullish — `||` covers both undefined and "".
|
|
@@ -366,8 +373,16 @@ export function formatFailedTask(r: TaskResult, cwd?: string): string[] {
|
|
|
366
373
|
parts.push(`[${isAbort ? "ABORTED" : "FAILED"}: ${failParts.join(" · ")}]`);
|
|
367
374
|
|
|
368
375
|
// Surface partial assistant output even when the task did not complete.
|
|
376
|
+
// Pass the dispatch-scoped spill bounds so a mid-task config change cannot
|
|
377
|
+
// widen (or narrow) the partial output the parent sees — the bounds captured
|
|
378
|
+
// when the task started are the ones the caller committed to.
|
|
369
379
|
if (r.output && r.output !== "(no output)") {
|
|
370
|
-
parts.push(
|
|
380
|
+
parts.push(
|
|
381
|
+
renderOutputForLLM(r.output, r.agent, {
|
|
382
|
+
thresholdChars: getOutputSpillThreshold(config),
|
|
383
|
+
tailChars: getOutputSpillTail(config),
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
if (r.sessionFile && isResumableSessionFile(r.sessionFile)) {
|
|
@@ -414,6 +429,8 @@ export function formatFailedTask(r: TaskResult, cwd?: string): string[] {
|
|
|
414
429
|
export function formatCompletedTask(
|
|
415
430
|
task: ResolvedTask,
|
|
416
431
|
result: TaskResult,
|
|
432
|
+
/** Dispatch-scoped snapshot for output-spill bounds. */
|
|
433
|
+
config?: import("./config.ts").DelegateConfig,
|
|
417
434
|
): string[] {
|
|
418
435
|
const parts: string[] = [];
|
|
419
436
|
// `|| task.sessionAction` covers action-only tasks (close/list/...) where prompt is
|
|
@@ -425,7 +442,7 @@ export function formatCompletedTask(
|
|
|
425
442
|
for (const w of task.warnings) parts.push(`[WARNING: ${w}]`);
|
|
426
443
|
}
|
|
427
444
|
if (result.error) {
|
|
428
|
-
parts.push(...formatFailedTask(result, task.cwd));
|
|
445
|
+
parts.push(...formatFailedTask(result, task.cwd, config));
|
|
429
446
|
} else {
|
|
430
447
|
const meta = [
|
|
431
448
|
`OK | ${fmtDuration(result.durationMs)} | ${fmtTokens(result.tokens)} tokens`,
|
|
@@ -434,15 +451,44 @@ export function formatCompletedTask(
|
|
|
434
451
|
const touched = relativeTouchedSummary(result.touchedFiles, task.cwd);
|
|
435
452
|
if (touched) meta.push(`touched (best-effort): ${touched}`);
|
|
436
453
|
parts.push(
|
|
437
|
-
`[${meta.join(" · ")}]\n\n${renderOutputForLLM(
|
|
454
|
+
`[${meta.join(" · ")}]\n\n${renderOutputForLLM(
|
|
455
|
+
result.output,
|
|
456
|
+
result.agent,
|
|
457
|
+
{
|
|
458
|
+
thresholdChars: getOutputSpillThreshold(config),
|
|
459
|
+
tailChars: getOutputSpillTail(config),
|
|
460
|
+
},
|
|
461
|
+
)}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (result.integration) {
|
|
465
|
+
const integration = result.integration;
|
|
466
|
+
parts.push(
|
|
467
|
+
`[INTEGRATION: ${integration.status} · proposed ${integration.proposedFiles.length} file(s) · applied ${integration.appliedFiles.length} file(s)]`,
|
|
438
468
|
);
|
|
469
|
+
if (integration.baselineRef)
|
|
470
|
+
parts.push(`baseline ref: ${integration.baselineRef}`);
|
|
471
|
+
if (integration.proposalRef)
|
|
472
|
+
parts.push(`proposal ref: ${integration.proposalRef}`);
|
|
473
|
+
if (integration.patchPath)
|
|
474
|
+
parts.push(`full patch: ${integration.patchPath}`);
|
|
475
|
+
if (integration.worktreePath)
|
|
476
|
+
parts.push(`conflict worktree: ${integration.worktreePath}`);
|
|
477
|
+
for (const conflict of integration.conflicts ?? []) {
|
|
478
|
+
parts.push(`conflict: ${conflict.path}: ${conflict.reason}`);
|
|
479
|
+
}
|
|
480
|
+
if (integration.status === "applied_unverified") {
|
|
481
|
+
parts.push(
|
|
482
|
+
'Changes were applied but not verified. Suggested next call: delegate({ tasks: [{ agent: "reviewer", workspace: "scratch", prompt: "Review the applied isolated changes and run the relevant tests." }] })',
|
|
483
|
+
);
|
|
484
|
+
}
|
|
439
485
|
}
|
|
440
486
|
return parts;
|
|
441
487
|
}
|
|
442
488
|
|
|
443
489
|
// ── Shared live-progress row helpers ───────────────────────────────────────
|
|
444
490
|
// These dedupe the per-task computations the LLM-facing poll view
|
|
445
|
-
// (
|
|
491
|
+
// (ticket-format.ts) and the TUI branches (render-branches) both need. Each is
|
|
446
492
|
// pure over TaskProgress/TaskResult, so it tests without a renderer.
|
|
447
493
|
|
|
448
494
|
/** The in-flight tool activity (no result yet), or null — the "current thing
|
|
@@ -499,7 +545,7 @@ export function waitingLabel(runningCount: number, cap: number): string {
|
|
|
499
545
|
|
|
500
546
|
/** Touched-files summary relative to cwd ("src/a.ts, src/b.ts"), or null when
|
|
501
547
|
* none resolve under cwd. Was byte-for-byte duplicated in formatCompletedTask
|
|
502
|
-
* and
|
|
548
|
+
* and ticket-format.ts. */
|
|
503
549
|
export function relativeTouchedSummary(
|
|
504
550
|
files: string[],
|
|
505
551
|
cwd: string,
|
package/host-cache.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generation-guarded async memo.
|
|
3
|
+
*
|
|
4
|
+
* Host deps are expensive (`ResourceLoader.reload()` is ~1.2s cold) and are
|
|
5
|
+
* shared across the parallel tasks of a single delegate dispatch, then thrown
|
|
6
|
+
* away so the next dispatch observes edits to auth, models, settings, and
|
|
7
|
+
* context files. That gives three requirements the plain
|
|
8
|
+
* `Map<string, Promise<T>>` pattern does not meet:
|
|
9
|
+
*
|
|
10
|
+
* - **in-flight dedup** — concurrent tasks with the same key must await one
|
|
11
|
+
* build, not start N reloads;
|
|
12
|
+
* - **generation guard** — an invalidation during a build must not let that
|
|
13
|
+
* older build install its now-stale value, nor let it delete the in-flight
|
|
14
|
+
* marker a newer build has since installed for the same key;
|
|
15
|
+
* - **one invalidation path** — every reset goes through `invalidate()`.
|
|
16
|
+
* Before this was factored out, two test-only helpers cleared the maps
|
|
17
|
+
* directly without bumping the generation, leaving exactly the stale-write
|
|
18
|
+
* window the guard exists to close.
|
|
19
|
+
*/
|
|
20
|
+
export class GenerationCache<T> {
|
|
21
|
+
private entries = new Map<string, T>();
|
|
22
|
+
private inflight = new Map<string, Promise<T>>();
|
|
23
|
+
private generation = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the cached value for `key`, joining an in-flight build or starting
|
|
27
|
+
* one. When `cacheable` is false the value is built and returned without ever
|
|
28
|
+
* being stored or shared.
|
|
29
|
+
*/
|
|
30
|
+
async resolve(
|
|
31
|
+
key: string,
|
|
32
|
+
cacheable: boolean,
|
|
33
|
+
build: () => Promise<T>,
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
if (!cacheable) return build();
|
|
36
|
+
|
|
37
|
+
const cached = this.entries.get(key);
|
|
38
|
+
if (cached !== undefined) return cached;
|
|
39
|
+
const pending = this.inflight.get(key);
|
|
40
|
+
if (pending) return pending;
|
|
41
|
+
|
|
42
|
+
const generation = this.generation;
|
|
43
|
+
const promise = build().then((value) => {
|
|
44
|
+
// A build that outlived its generation is stale by definition; return it
|
|
45
|
+
// to its own caller but never publish it.
|
|
46
|
+
if (this.generation === generation) this.entries.set(key, value);
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
this.inflight.set(key, promise);
|
|
50
|
+
try {
|
|
51
|
+
return await promise;
|
|
52
|
+
} finally {
|
|
53
|
+
// An invalidation can let a newer generation install its own in-flight
|
|
54
|
+
// build for this key. Never let the older promise delete that marker.
|
|
55
|
+
if (this.inflight.get(key) === promise) this.inflight.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Drop every cached and in-flight value, invalidating builds already running. */
|
|
60
|
+
invalidate(): void {
|
|
61
|
+
this.generation++;
|
|
62
|
+
this.entries.clear();
|
|
63
|
+
this.inflight.clear();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Cached values only — in-flight builds are not observable here. */
|
|
67
|
+
values(): Iterable<T> {
|
|
68
|
+
return this.entries.values();
|
|
69
|
+
}
|
|
70
|
+
}
|