@shanepadgett/tau-agent 0.37.0 → 0.39.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/docs/context.md +2 -2
- package/extensions/attention/README.md +3 -2
- package/extensions/attention/index.ts +1 -1
- package/extensions/auto-compact/README.md +9 -0
- package/extensions/auto-compact/index.ts +100 -0
- package/extensions/auto-compact/settings.ts +21 -0
- package/extensions/cache-diagnostics/index.ts +1 -1
- package/extensions/commit/README.md +2 -2
- package/extensions/commit/commit-plan.ts +13 -2
- package/extensions/commit/index.ts +18 -85
- package/extensions/commit/review-ui.ts +535 -132
- package/extensions/soul/README.md +6 -14
- package/extensions/soul/index.ts +6 -19
- package/extensions/soul/overseer.ts +272 -0
- package/extensions/soul/prompt.ts +102 -39
- package/extensions/soul/settings.ts +22 -9
- package/extensions/subagent/agents/context-sync.md +5 -6
- package/extensions/subagent/agents/scout.md +0 -3
- package/extensions/tau-help/help.md +6 -6
- package/extensions/tool-approval/index.ts +0 -3
- package/package.json +5 -5
- package/schemas/tau.schema.json +29 -19
- package/extensions/checkpoint/README.md +0 -9
- package/extensions/checkpoint/checkpoint-budget.ts +0 -66
- package/extensions/checkpoint/checkpoint.ts +0 -279
- package/extensions/checkpoint/index.ts +0 -102
- package/extensions/checkpoint/messages.ts +0 -170
- package/extensions/checkpoint/prompt.ts +0 -24
- package/extensions/checkpoint/settings.ts +0 -27
- package/shared/checkpoint-visibility.ts +0 -9
package/docs/context.md
CHANGED
|
@@ -15,7 +15,7 @@ The catalog has three levels:
|
|
|
15
15
|
- **Concept**: a coherent subsystem or capability inside a domain. Each concept is one TOML file.
|
|
16
16
|
- **Entry**: a selectable work scope inside that concept. Each TOML section defines one entry.
|
|
17
17
|
|
|
18
|
-
Domain slugs (after `NN_`), concept filenames, and entry section names use lowercase kebab-case. Orders are contiguous from `01` with no gaps or duplicate slugs. Entry ids use the slug (`extensions/
|
|
18
|
+
Domain slugs (after `NN_`), concept filenames, and entry section names use lowercase kebab-case. Orders are contiguous from `01` with no gaps or duplicate slugs. Entry ids use the slug (`extensions/patch/parse`), not the folder prefix.
|
|
19
19
|
|
|
20
20
|
```toml
|
|
21
21
|
name = "Checkout"
|
|
@@ -42,7 +42,7 @@ Saved as `.pi/contexts/01_commerce/checkout.toml`, these entries have the IDs `c
|
|
|
42
42
|
|
|
43
43
|
## Build a useful taxonomy
|
|
44
44
|
|
|
45
|
-
Each entry is a **work pack**: enough primary material that selecting only that entry lets an agent start one recurring job with little search. Taxonomy groups packs; it is not a file index. Gold-standard example: `.pi/contexts/01_extensions/
|
|
45
|
+
Each entry is a **work pack**: enough primary material that selecting only that entry lets an agent start one recurring job with little search. Taxonomy groups packs; it is not a file index. Gold-standard example: `.pi/contexts/01_extensions/patch.toml`.
|
|
46
46
|
|
|
47
47
|
Classify from the top down:
|
|
48
48
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Attention
|
|
2
2
|
|
|
3
|
-
Sends a terminal-driven attention notification when Tau is ready for input, finishes
|
|
3
|
+
Sends a terminal-driven attention notification when Tau is ready for input, finishes a compaction without an automatic continuation, or summarizes an abandoned branch during tree navigation.
|
|
4
4
|
|
|
5
5
|
## Behavior
|
|
6
6
|
|
|
7
7
|
- Emits an attention notification after the agent settles with no automatic continuation pending.
|
|
8
8
|
- Waits for automatic post-turn checks before deciding whether the agent is ready for input.
|
|
9
|
-
- Emits an attention notification on `session_compact
|
|
9
|
+
- Emits an attention notification on `session_compact` unless another extension has an active attention hold.
|
|
10
|
+
- Defers settlement and compaction notifications while an attention hold is active.
|
|
10
11
|
- Emits an attention notification on `session_tree` when it includes a branch summary.
|
|
11
12
|
- Listens for shared event `tau:agent.blocked` when Tau is waiting on user input.
|
|
12
13
|
- Uses the terminal or host OS notification path that best fits the current environment.
|
|
@@ -104,7 +104,7 @@ export default function attentionExtension(pi: ExtensionAPI): void {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
pi.on("session_compact", (_event, ctx) => {
|
|
107
|
-
if (ctx.mode === "print") return;
|
|
107
|
+
if (ctx.mode === "print" || holds.size > 0) return;
|
|
108
108
|
notify({ title: DEFAULT_TITLE, body: COMPACTION_BODY });
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Auto Compact
|
|
2
|
+
|
|
3
|
+
Compacts long conversations before the next model turn when their context reaches an absolute token limit. Active work resumes through a hidden continuation message after Pi's native compaction finishes.
|
|
4
|
+
|
|
5
|
+
Set `extensions.autoCompact.tokenLimit` to change the limit. It defaults to 175,000 context tokens for every model. Pi still shows its native collapsed compaction entry in the chat.
|
|
6
|
+
|
|
7
|
+
After changing this extension, run `/reload` before testing it.
|
|
8
|
+
|
|
9
|
+
Automatic compaction holds attention notifications while it runs and while the hidden continuation resumes the interrupted work. The notification is released only after that resumed work settles.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
3
|
+
import { emitTauEvent } from "../../shared/events.ts";
|
|
4
|
+
import autoCompactSettings, { DEFAULT_AUTO_COMPACT_TOKEN_LIMIT } from "./settings.ts";
|
|
5
|
+
|
|
6
|
+
const CONTINUATION_TYPE = "tau.auto-compact";
|
|
7
|
+
const CONTINUATION_MESSAGE =
|
|
8
|
+
"Continue the current work directly from the compacted context. Do not mention compaction or wait for user input.";
|
|
9
|
+
|
|
10
|
+
export default function autoCompactExtension(pi: ExtensionAPI): void {
|
|
11
|
+
let tokenLimit = DEFAULT_AUTO_COMPACT_TOKEN_LIMIT;
|
|
12
|
+
let armed = true;
|
|
13
|
+
let compacting = false;
|
|
14
|
+
let sessionVersion = 0;
|
|
15
|
+
let attentionHoldSequence = 0;
|
|
16
|
+
let attentionHoldId: string | undefined;
|
|
17
|
+
let continuationPending = false;
|
|
18
|
+
|
|
19
|
+
function releaseAttentionHold(disposition: "notify" | "discard"): void {
|
|
20
|
+
const holdId = attentionHoldId;
|
|
21
|
+
attentionHoldId = undefined;
|
|
22
|
+
if (holdId) emitTauEvent(pi, "tau:attention.hold.release", { id: holdId, disposition });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
26
|
+
const version = ++sessionVersion;
|
|
27
|
+
const settings = await loadTauExtensionSettings(ctx, autoCompactSettings);
|
|
28
|
+
if (version !== sessionVersion) return;
|
|
29
|
+
tokenLimit = settings.tokenLimit;
|
|
30
|
+
armed = true;
|
|
31
|
+
compacting = false;
|
|
32
|
+
attentionHoldSequence = 0;
|
|
33
|
+
attentionHoldId = undefined;
|
|
34
|
+
continuationPending = false;
|
|
35
|
+
});
|
|
36
|
+
pi.on("session_shutdown", () => {
|
|
37
|
+
sessionVersion++;
|
|
38
|
+
armed = true;
|
|
39
|
+
compacting = false;
|
|
40
|
+
attentionHoldId = undefined;
|
|
41
|
+
continuationPending = false;
|
|
42
|
+
});
|
|
43
|
+
pi.on("session_tree", () => {
|
|
44
|
+
armed = true;
|
|
45
|
+
});
|
|
46
|
+
pi.on("session_compact", () => {
|
|
47
|
+
armed = false;
|
|
48
|
+
});
|
|
49
|
+
pi.on("agent_settled", () => {
|
|
50
|
+
if (!continuationPending) return;
|
|
51
|
+
continuationPending = false;
|
|
52
|
+
releaseAttentionHold("notify");
|
|
53
|
+
});
|
|
54
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
55
|
+
if (compacting) return;
|
|
56
|
+
|
|
57
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
58
|
+
if (tokens === undefined || tokens === null) return;
|
|
59
|
+
if (tokens < tokenLimit) {
|
|
60
|
+
armed = true;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!armed) return;
|
|
64
|
+
|
|
65
|
+
armed = false;
|
|
66
|
+
compacting = true;
|
|
67
|
+
if (ctx.mode !== "print") {
|
|
68
|
+
attentionHoldId = `auto-compact:${++attentionHoldSequence}`;
|
|
69
|
+
emitTauEvent(pi, "tau:attention.hold.acquire", { id: attentionHoldId });
|
|
70
|
+
}
|
|
71
|
+
const version = sessionVersion;
|
|
72
|
+
const continueWork = (): void => {
|
|
73
|
+
if (version !== sessionVersion) return;
|
|
74
|
+
compacting = false;
|
|
75
|
+
continuationPending = true;
|
|
76
|
+
pi.sendMessage(
|
|
77
|
+
{
|
|
78
|
+
customType: CONTINUATION_TYPE,
|
|
79
|
+
content: CONTINUATION_MESSAGE,
|
|
80
|
+
display: false,
|
|
81
|
+
details: { v: 1, kind: "auto-compact.continuation", source: "auto-compact" },
|
|
82
|
+
},
|
|
83
|
+
{ triggerTurn: true },
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
ctx.compact({
|
|
88
|
+
onComplete: continueWork,
|
|
89
|
+
onError: (error) => {
|
|
90
|
+
if (version !== sessionVersion) return;
|
|
91
|
+
compacting = false;
|
|
92
|
+
if (error.name === "AbortError" || error.message === "Compaction cancelled") {
|
|
93
|
+
releaseAttentionHold("notify");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
continueWork();
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_AUTO_COMPACT_TOKEN_LIMIT = 175_000;
|
|
5
|
+
|
|
6
|
+
export default defineTauExtensionSettings({
|
|
7
|
+
key: "autoCompact",
|
|
8
|
+
defaults: {
|
|
9
|
+
tokenLimit: DEFAULT_AUTO_COMPACT_TOKEN_LIMIT,
|
|
10
|
+
},
|
|
11
|
+
schema: Type.Object(
|
|
12
|
+
{
|
|
13
|
+
tokenLimit: Type.Integer({
|
|
14
|
+
minimum: 1,
|
|
15
|
+
default: DEFAULT_AUTO_COMPACT_TOKEN_LIMIT,
|
|
16
|
+
description: "Absolute context-token count that triggers compaction before the next model turn.",
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
{ additionalProperties: false },
|
|
20
|
+
),
|
|
21
|
+
});
|
|
@@ -483,7 +483,7 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
|
|
|
483
483
|
);
|
|
484
484
|
pi.on("session_tree", () => addMarker("session-tree"));
|
|
485
485
|
pi.on("tool_execution_end", (event) => {
|
|
486
|
-
if (event.toolName !== "
|
|
486
|
+
if (event.toolName !== "load_tools") return;
|
|
487
487
|
return addMarker("cache-affecting-tool", { tool: event.toolName, isError: event.isError });
|
|
488
488
|
});
|
|
489
489
|
}
|
|
@@ -10,6 +10,6 @@ Generate a semantic commit plan from current repository changes, review it, then
|
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
|
-
`/commit`
|
|
13
|
+
`/commit` takes over the editor for the full run. It gathers change evidence, asks a model for a small set of meaningful conventional commits, lets you review and edit the plan, commits the approved groups, then asks whether to run `git push`.
|
|
14
14
|
|
|
15
|
-
Unassigned files are left uncommitted.
|
|
15
|
+
Unassigned files are left uncommitted. Progress and prompts stay in the commit panel until the flow ends; success still uses the normal notify messages.
|
|
@@ -45,11 +45,16 @@ interface CommitPlanToolCommit {
|
|
|
45
45
|
files: readonly number[];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface CommitGenerationOptions {
|
|
49
|
+
onStatus?: (status: string) => void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
export async function generatePlan(
|
|
49
53
|
ctx: ExtensionCommandContext,
|
|
50
54
|
evidence: CommitEvidence,
|
|
51
55
|
previousPlan: readonly CommitGroup[] = [],
|
|
52
56
|
regenerationNote = "",
|
|
57
|
+
options?: CommitGenerationOptions,
|
|
53
58
|
): Promise<CommitGroup[]> {
|
|
54
59
|
const prompt = buildPlanPrompt(evidence, previousPlan, regenerationNote);
|
|
55
60
|
return generateToolValidated(
|
|
@@ -68,7 +73,11 @@ export async function generatePlan(
|
|
|
68
73
|
"Previous response:",
|
|
69
74
|
text,
|
|
70
75
|
].join("\n"),
|
|
71
|
-
{
|
|
76
|
+
{
|
|
77
|
+
statusKey: options?.onStatus ? undefined : "commit",
|
|
78
|
+
onStatus: options?.onStatus,
|
|
79
|
+
notifyOnFallback: true,
|
|
80
|
+
},
|
|
72
81
|
);
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -79,6 +88,7 @@ export async function regenerateMessage(
|
|
|
79
88
|
previousPlan: readonly CommitGroup[] = [],
|
|
80
89
|
selectedGroupId: string | undefined = undefined,
|
|
81
90
|
regenerationNote = "",
|
|
91
|
+
options?: CommitGenerationOptions,
|
|
82
92
|
): Promise<string> {
|
|
83
93
|
const selected = evidence.files.filter((file) => files.includes(file.path));
|
|
84
94
|
const prompt = buildMessagePrompt(evidence, selected, previousPlan, selectedGroupId, regenerationNote);
|
|
@@ -89,7 +99,8 @@ export async function regenerateMessage(
|
|
|
89
99
|
requireCommitMessage,
|
|
90
100
|
undefined,
|
|
91
101
|
{
|
|
92
|
-
statusKey: "commit",
|
|
102
|
+
statusKey: options?.onStatus ? undefined : "commit",
|
|
103
|
+
onStatus: options?.onStatus,
|
|
93
104
|
notifyOnFallback: true,
|
|
94
105
|
},
|
|
95
106
|
);
|
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { createGitRunner, type GitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
2
|
+
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
4
3
|
import { errorText } from "../../shared/text.ts";
|
|
5
4
|
import { type CommitPlanState, generatePlan } from "./commit-plan.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
commitStaged,
|
|
9
|
-
computeWorktreeSignature,
|
|
10
|
-
type DirtyFile,
|
|
11
|
-
loadChangeSet,
|
|
12
|
-
stageFilesOnly,
|
|
13
|
-
} from "./git-change-set.ts";
|
|
14
|
-
import { reviewPlan } from "./review-ui.ts";
|
|
5
|
+
import { assertCommittableState, computeWorktreeSignature, loadChangeSet } from "./git-change-set.ts";
|
|
6
|
+
import { executeCommitPlan, PartialCommitError, runCommitFlow } from "./review-ui.ts";
|
|
15
7
|
|
|
16
8
|
const COMMIT_MARKER_TYPE = "tau.commit";
|
|
17
|
-
const PUSH_TIMEOUT_MS = 120_000;
|
|
18
|
-
|
|
19
|
-
interface CommitMarker {
|
|
20
|
-
hash: string;
|
|
21
|
-
subject: string;
|
|
22
|
-
timestamp: number;
|
|
23
|
-
}
|
|
24
9
|
|
|
25
10
|
export default function commitExtension(pi: ExtensionAPI): void {
|
|
26
11
|
pi.registerCommand("commit", {
|
|
27
12
|
description: "Generate semantic commit groups and commit selected repository changes",
|
|
28
13
|
handler: async (_args, ctx) => {
|
|
29
14
|
await ctx.waitForIdle();
|
|
30
|
-
ctx.ui.setStatus("commit", "preparing commit plan");
|
|
31
|
-
|
|
32
15
|
try {
|
|
33
16
|
await runCommit(pi, ctx);
|
|
34
17
|
} catch (error) {
|
|
35
18
|
ctx.ui.notify(`Commit failed: ${errorText(error)}`, "error");
|
|
36
|
-
} finally {
|
|
37
|
-
ctx.ui.setStatus("commit", undefined);
|
|
38
19
|
}
|
|
39
20
|
},
|
|
40
21
|
});
|
|
@@ -56,83 +37,35 @@ async function runCommit(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promis
|
|
|
56
37
|
return;
|
|
57
38
|
}
|
|
58
39
|
|
|
40
|
+
if (ctx.hasUI) {
|
|
41
|
+
await runCommitFlow(pi, ctx, git, repo.root, COMMIT_MARKER_TYPE);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
59
45
|
const evidence = await loadChangeSet(git, repo.root, ctx.sessionManager.getBranch(), COMMIT_MARKER_TYPE);
|
|
60
46
|
assertCommittableState(evidence.files);
|
|
61
|
-
|
|
47
|
+
const state: CommitPlanState = {
|
|
62
48
|
files: evidence.files,
|
|
63
49
|
worktreeSignature: await computeWorktreeSignature(git, repo.root, evidence.files),
|
|
64
50
|
groups: await generatePlan(ctx, evidence),
|
|
65
51
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const completed = await executePlan(pi, ctx, git, repo.root, state);
|
|
75
|
-
if (completed.length === 0) return;
|
|
76
|
-
|
|
77
|
-
if (ctx.hasUI && (await ctx.ui.confirm("Push after commits?", "Run `git push` after all commits succeeded?"))) {
|
|
78
|
-
ctx.ui.setStatus("commit", "pushing");
|
|
79
|
-
await git.run(["push"], { cwd: repo.root, timeout: PUSH_TIMEOUT_MS });
|
|
52
|
+
try {
|
|
53
|
+
const completed = await executeCommitPlan(pi, git, repo.root, state, COMMIT_MARKER_TYPE);
|
|
54
|
+
if (completed.length === 0) {
|
|
55
|
+
ctx.ui.notify("No commit groups to execute.", "info");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
80
58
|
ctx.ui.notify(
|
|
81
|
-
`Committed
|
|
59
|
+
`Committed ${completed.length} commit(s): ${completed.map((item) => item.hash).join(", ")}`,
|
|
82
60
|
"info",
|
|
83
61
|
);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
ctx.ui.notify(`Committed ${completed.length} commit(s): ${completed.map((item) => item.hash).join(", ")}`, "info");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function executePlan(
|
|
91
|
-
pi: ExtensionAPI,
|
|
92
|
-
ctx: ExtensionCommandContext,
|
|
93
|
-
git: GitRunner,
|
|
94
|
-
root: string,
|
|
95
|
-
state: CommitPlanState,
|
|
96
|
-
): Promise<CommitMarker[]> {
|
|
97
|
-
const groups = state.groups.filter((group) => group.files.length > 0);
|
|
98
|
-
if (groups.length === 0) {
|
|
99
|
-
ctx.ui.notify("No commit groups to execute.", "info");
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if ((await computeWorktreeSignature(git, root, state.files)) !== state.worktreeSignature) {
|
|
104
|
-
throw new Error("Working tree changed during commit review. Regenerate the commit plan and try again.");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const filesByPath = new Map(state.files.map((file) => [file.path, file]));
|
|
108
|
-
const completed: CommitMarker[] = [];
|
|
109
|
-
try {
|
|
110
|
-
for (const group of groups) {
|
|
111
|
-
const subject = group.message.split("\n")[0] ?? group.message;
|
|
112
|
-
ctx.ui.setStatus("commit", `committing ${subject}`);
|
|
113
|
-
await stageFilesOnly(
|
|
114
|
-
git,
|
|
115
|
-
root,
|
|
116
|
-
group.files.flatMap((path) => knownFile(filesByPath, path)),
|
|
117
|
-
);
|
|
118
|
-
const hash = await commitStaged(git, root, group.message);
|
|
119
|
-
const marker = { hash, subject, timestamp: Date.now() };
|
|
120
|
-
completed.push(marker);
|
|
121
|
-
pi.appendEntry<CommitMarker>(COMMIT_MARKER_TYPE, marker);
|
|
122
|
-
}
|
|
123
62
|
} catch (error) {
|
|
124
|
-
if (
|
|
63
|
+
if (error instanceof PartialCommitError) {
|
|
125
64
|
ctx.ui.notify(
|
|
126
|
-
`Partially committed ${completed.length}: ${completed.map((item) => item.hash).join(", ")}; then failed: ${
|
|
65
|
+
`Partially committed ${error.completed.length}: ${error.completed.map((item) => item.hash).join(", ")}; then failed: ${error.message}`,
|
|
127
66
|
"warning",
|
|
128
67
|
);
|
|
129
68
|
}
|
|
130
69
|
throw error;
|
|
131
70
|
}
|
|
132
|
-
return completed;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function knownFile(filesByPath: ReadonlyMap<string, DirtyFile>, path: string): DirtyFile[] {
|
|
136
|
-
const file = filesByPath.get(path);
|
|
137
|
-
return file ? [file] : [];
|
|
138
71
|
}
|