@tt-a1i/openpi 0.1.1 → 0.2.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/README.md +37 -22
- package/SETUP.md +8 -6
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/index.ts +12 -0
- package/extensions/setup/index.ts +93 -7
- package/extensions/shared/child-session.ts +40 -4
- package/extensions/shared/setup-config.ts +22 -0
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +15 -0
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +1 -1
- package/extensions/tasks/index.ts +39 -12
- package/extensions/ui-customization/footer.ts +6 -1
- package/extensions/workflows/graph-projection.ts +6 -4
- package/extensions/workflows/index.ts +16 -1
- package/extensions/workflows/invocation-ledger.ts +8 -2
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +10 -40
- package/extensions/workflows/replay-safety.ts +9 -8
- package/package.json +10 -10
- package/skills/subagents/SKILL.md +6 -0
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -11,15 +11,29 @@ export function createDeferredResultDelivery<T extends { id: string }>() {
|
|
|
11
11
|
return {
|
|
12
12
|
defer(result: T) {
|
|
13
13
|
pending.set(result.id, result);
|
|
14
|
+
return pending.size;
|
|
14
15
|
},
|
|
15
16
|
consume(ids: Iterable<string>) {
|
|
16
17
|
for (const id of ids) pending.delete(id);
|
|
17
18
|
},
|
|
18
|
-
drain() {
|
|
19
|
-
const results = [
|
|
20
|
-
pending
|
|
19
|
+
drain(maxResults = Number.POSITIVE_INFINITY) {
|
|
20
|
+
const results: T[] = [];
|
|
21
|
+
for (const [id, result] of pending) {
|
|
22
|
+
if (results.length >= maxResults) break;
|
|
23
|
+
results.push(result);
|
|
24
|
+
pending.delete(id);
|
|
25
|
+
}
|
|
21
26
|
return results;
|
|
22
27
|
},
|
|
28
|
+
restore(results: readonly T[]) {
|
|
29
|
+
const current = [...pending.values()];
|
|
30
|
+
pending.clear();
|
|
31
|
+
for (const result of results) pending.set(result.id, result);
|
|
32
|
+
for (const result of current) pending.set(result.id, result);
|
|
33
|
+
},
|
|
34
|
+
size() {
|
|
35
|
+
return pending.size;
|
|
36
|
+
},
|
|
23
37
|
clear() {
|
|
24
38
|
pending.clear();
|
|
25
39
|
},
|
|
@@ -36,6 +50,65 @@ export function createDeferredResultDelivery<T extends { id: string }>() {
|
|
|
36
50
|
* result in context — carried alongside the user's next message — without
|
|
37
51
|
* demanding a reply.
|
|
38
52
|
*/
|
|
53
|
+
export interface IdleResultBatcherOptions<TimerHandle> {
|
|
54
|
+
readonly delayMs: number;
|
|
55
|
+
readonly isIdle: () => boolean;
|
|
56
|
+
readonly flush: (wake: boolean) => void;
|
|
57
|
+
readonly startTimer: (callback: () => void, delayMs: number) => TimerHandle;
|
|
58
|
+
readonly clearTimer: (timer: TimerHandle) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Coalesce settlements that arrive while the agent is idle without turning
|
|
63
|
+
* the window into a sliding delay. A token rejects stale callbacks after
|
|
64
|
+
* cancellation, including callbacks already queued by the host event loop.
|
|
65
|
+
*/
|
|
66
|
+
export function createIdleResultBatcher<TimerHandle>(
|
|
67
|
+
options: IdleResultBatcherOptions<TimerHandle>,
|
|
68
|
+
) {
|
|
69
|
+
let timer: TimerHandle | undefined;
|
|
70
|
+
let active: symbol | undefined;
|
|
71
|
+
|
|
72
|
+
const cancel = () => {
|
|
73
|
+
active = undefined;
|
|
74
|
+
if (timer !== undefined) options.clearTimer(timer);
|
|
75
|
+
timer = undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
schedule() {
|
|
80
|
+
if (active !== undefined) return;
|
|
81
|
+
const token = Symbol("idle-result-batch");
|
|
82
|
+
active = token;
|
|
83
|
+
timer = options.startTimer(() => {
|
|
84
|
+
if (active !== token) return;
|
|
85
|
+
active = undefined;
|
|
86
|
+
timer = undefined;
|
|
87
|
+
if (options.isIdle()) options.flush(true);
|
|
88
|
+
}, options.delayMs);
|
|
89
|
+
},
|
|
90
|
+
flushNow() {
|
|
91
|
+
const wake = options.isIdle();
|
|
92
|
+
cancel();
|
|
93
|
+
options.flush(wake);
|
|
94
|
+
},
|
|
95
|
+
flushWithoutWake() {
|
|
96
|
+
cancel();
|
|
97
|
+
options.flush(false);
|
|
98
|
+
},
|
|
99
|
+
clear: cancel,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hasTerminalCapacity(options: {
|
|
104
|
+
readonly running: number;
|
|
105
|
+
readonly pending: number;
|
|
106
|
+
readonly reserved: number;
|
|
107
|
+
readonly maximum: number;
|
|
108
|
+
}) {
|
|
109
|
+
return options.running + options.pending + options.reserved < options.maximum;
|
|
110
|
+
}
|
|
111
|
+
|
|
39
112
|
export function resultDeliveryOptions(wake: boolean) {
|
|
40
113
|
return wake
|
|
41
114
|
? ({ deliverAs: "followUp", triggerTurn: true } as const)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text, truncateToWidth
|
|
2
|
+
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { sanitizeText } from "./output-view.ts";
|
|
4
4
|
|
|
5
5
|
const STREAM_PREVIEW_LINES = 2;
|
|
@@ -102,6 +102,57 @@ function fixedRows(rows: string[]): Component {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
interface TerminalBatchResult {
|
|
106
|
+
readonly id: string;
|
|
107
|
+
readonly title: string;
|
|
108
|
+
readonly status: string;
|
|
109
|
+
readonly exitCode?: number;
|
|
110
|
+
readonly signal?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Render a batch as a truthful per-terminal summary; expand reveals raw logs. */
|
|
114
|
+
export function renderTerminalBatchResult(
|
|
115
|
+
content: string,
|
|
116
|
+
expanded: boolean,
|
|
117
|
+
theme: Theme,
|
|
118
|
+
results: readonly TerminalBatchResult[],
|
|
119
|
+
omitted = 0,
|
|
120
|
+
): Component {
|
|
121
|
+
if (expanded) return new Text(sanitizeText(content), 0, 0);
|
|
122
|
+
const rows = [
|
|
123
|
+
theme.fg("accent", `${results.length} background terminals completed`),
|
|
124
|
+
...results.map((result) => {
|
|
125
|
+
const failed =
|
|
126
|
+
result.status === "failed" || result.status === "timed_out";
|
|
127
|
+
const icon = failed
|
|
128
|
+
? theme.fg("error", "x")
|
|
129
|
+
: result.status === "killed"
|
|
130
|
+
? theme.fg("muted", "■")
|
|
131
|
+
: theme.fg("success", "■");
|
|
132
|
+
const how =
|
|
133
|
+
result.status === "timed_out"
|
|
134
|
+
? "timed out"
|
|
135
|
+
: result.status === "killed"
|
|
136
|
+
? "killed"
|
|
137
|
+
: (result.signal ?? `exit ${result.exitCode ?? "?"}`);
|
|
138
|
+
return `${icon} ${theme.fg("accent", result.id)}${theme.fg("muted", ` · ${result.title} · ${how}`)}`;
|
|
139
|
+
}),
|
|
140
|
+
...(omitted > 0
|
|
141
|
+
? [
|
|
142
|
+
theme.fg(
|
|
143
|
+
"dim",
|
|
144
|
+
`… ${omitted} older result${omitted === 1 ? "" : "s"} omitted`,
|
|
145
|
+
),
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
theme.fg(
|
|
149
|
+
"dim",
|
|
150
|
+
`… logs hidden · ${keyHint("app.tools.expand", "to expand")}`,
|
|
151
|
+
),
|
|
152
|
+
];
|
|
153
|
+
return fixedRows(rows);
|
|
154
|
+
}
|
|
155
|
+
|
|
105
156
|
/** Render terminal results compactly by default; expanded mode preserves all text. */
|
|
106
157
|
export function renderTerminalResult(
|
|
107
158
|
content: string,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
loadSetupConfig,
|
|
6
|
+
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
7
|
+
type MyPiSetupConfig,
|
|
8
|
+
} from "../shared/setup-config.ts";
|
|
9
|
+
import {
|
|
10
|
+
getLoadedOpenPiCapabilities,
|
|
11
|
+
loadOpenPiCapabilities,
|
|
12
|
+
OPENPI_CAPABILITY_GROUPS,
|
|
13
|
+
OPENPI_CAPABILITY_NAMES,
|
|
14
|
+
OPENPI_TOOL_SURFACE,
|
|
15
|
+
type OpenPiCapability,
|
|
16
|
+
patchOwnedTools,
|
|
17
|
+
resetOpenPiToolSurface,
|
|
18
|
+
} from "../shared/tool-surface.ts";
|
|
19
|
+
|
|
20
|
+
const CapabilitySchema = Type.Enum(OPENPI_CAPABILITY_NAMES);
|
|
21
|
+
|
|
22
|
+
const OpenPiLoadToolsParameters = Type.Object({
|
|
23
|
+
groups: Type.Optional(
|
|
24
|
+
Type.Array(CapabilitySchema, {
|
|
25
|
+
minItems: 1,
|
|
26
|
+
maxItems: OPENPI_CAPABILITY_NAMES.length,
|
|
27
|
+
uniqueItems: true,
|
|
28
|
+
description: "Groups to load; omit to list.",
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type OpenPiLoadToolsInput = Static<typeof OpenPiLoadToolsParameters>;
|
|
34
|
+
|
|
35
|
+
const CAPABILITY_INTENT = {
|
|
36
|
+
search:
|
|
37
|
+
/\b(?:use|run)\s+(?:fd|rg)\b|\buse\s+(?:structured\s+)?(?:(?:file|code|content)\s+)?search\b|\b(?:structured|fast)\s+(?:file|code|content)\s+search\b|(?:使用|运行).{0,8}(?:fd|rg)|结构化(?:文件|代码|内容)搜索/iu,
|
|
38
|
+
delegate:
|
|
39
|
+
/\b(?:use|spawn|run)\s+(?:an?\s+|multiple\s+|several\s+|two\s+)?(?:pi\s+)?subagents?\b|(?:^|[.!?]\s+)(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:can|could|would)\s+you\s+(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\bparallel\s+agents?\b|(?:使用|启动|调用|来|开).{0,8}子代理|(?:多个?|多路)子代理|并行.{0,8}(?:代理|agent)|委派.{0,6}任务/iu,
|
|
40
|
+
workflow:
|
|
41
|
+
/\b(?:use|run|create|build)\s+(?:(?:an?|the)\s+)?(?:openpi\s+)?workflow\b|(?:使用|运行|创建|构建).{0,8}工作流/iu,
|
|
42
|
+
background:
|
|
43
|
+
/\b(?:run|start|keep)\b.{0,40}\b(?:in the background|background\s+(?:process|terminal|job))\b|后台.{0,8}(?:运行|进程|终端|任务)/iu,
|
|
44
|
+
session:
|
|
45
|
+
/\b(?:create|set|update|track)\s+(?:an?\s+)?(?:session\s+)?(?:goal|task list|tasks)\b|(?:设置|创建|更新|跟踪|追踪).{0,8}(?:目标|任务)/iu,
|
|
46
|
+
} as const satisfies Record<OpenPiCapability, RegExp>;
|
|
47
|
+
|
|
48
|
+
const CAPABILITY_GATEWAY_INTENT =
|
|
49
|
+
/\bopenpi\s+(?:capabilit(?:y|ies)|tools?|features?)\b|openpi.{0,8}(?:能力|工具|功能)/iu;
|
|
50
|
+
|
|
51
|
+
const CONDITIONAL_OR_NEGATED_INTENT =
|
|
52
|
+
/^(?:\s*(?:only\s+)?(?:if|when|unless|before|in case)\b)|\b(?:do not|don't|cannot|can't|not|no|never|avoid)\b|\b(?:if|unless)\b|\bwhen\s+(?:needed|required|necessary)\b|(?:如果|若|假如|除非|仅当|需要时|不要|不能|不用|不必|无需|避免|请勿|禁止)/iu;
|
|
53
|
+
|
|
54
|
+
const CAPABILITY_SKILLS: Partial<Record<OpenPiCapability, string>> = {
|
|
55
|
+
delegate: fileURLToPath(
|
|
56
|
+
new URL("../../skills/subagents/SKILL.md", import.meta.url),
|
|
57
|
+
),
|
|
58
|
+
workflow: fileURLToPath(
|
|
59
|
+
new URL("../../skills/workflows/SKILL.md", import.meta.url),
|
|
60
|
+
),
|
|
61
|
+
background: fileURLToPath(
|
|
62
|
+
new URL("../../skills/background-terminals/SKILL.md", import.meta.url),
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function capabilitiesRequestedByPrompt(prompt: string) {
|
|
67
|
+
const clauses = prompt.split(/[\n.!?。!?;;]+/u);
|
|
68
|
+
return OPENPI_CAPABILITY_NAMES.filter((capability) =>
|
|
69
|
+
clauses.some(
|
|
70
|
+
(clause) =>
|
|
71
|
+
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
|
|
72
|
+
CAPABILITY_INTENT[capability].test(clause),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requestsCapabilityGateway(prompt: string) {
|
|
78
|
+
return prompt
|
|
79
|
+
.split(/[\n.!?。!?;;]+/u)
|
|
80
|
+
.some(
|
|
81
|
+
(clause) =>
|
|
82
|
+
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
|
|
83
|
+
CAPABILITY_GATEWAY_INTENT.test(clause),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function capabilitySkillPaths(capabilities: readonly OpenPiCapability[]) {
|
|
88
|
+
return capabilities.flatMap((capability) => {
|
|
89
|
+
const skill = CAPABILITY_SKILLS[capability];
|
|
90
|
+
return skill ? [skill] : [];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function skillGuidance(capabilities: readonly OpenPiCapability[]) {
|
|
95
|
+
const skills = capabilitySkillPaths(capabilities);
|
|
96
|
+
return skills.length > 0
|
|
97
|
+
? `Before first use, read the matching OpenPI capability guidance: ${skills.join(", ")}.`
|
|
98
|
+
: "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface CapabilityExtensionDependencies {
|
|
102
|
+
readonly loadConfig: () => Pick<MyPiSetupConfig, "capabilities">;
|
|
103
|
+
readonly sourcePath?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createCapabilitiesExtension(
|
|
107
|
+
dependencies: CapabilityExtensionDependencies = {
|
|
108
|
+
loadConfig: loadSetupConfig,
|
|
109
|
+
},
|
|
110
|
+
) {
|
|
111
|
+
return function capabilities(pi: ExtensionAPI) {
|
|
112
|
+
const reconcileDiscoveryGateway = () => {
|
|
113
|
+
const adaptive =
|
|
114
|
+
dependencies.loadConfig().capabilities.discovery === "adaptive";
|
|
115
|
+
patchOwnedTools(pi, "capabilities", {
|
|
116
|
+
...(adaptive
|
|
117
|
+
? { enable: OPENPI_TOOL_SURFACE.capabilities.entry }
|
|
118
|
+
: { disable: OPENPI_TOOL_SURFACE.capabilities.entry }),
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, reconcileDiscoveryGateway);
|
|
123
|
+
|
|
124
|
+
pi.on("session_start", () => {
|
|
125
|
+
resetOpenPiToolSurface(
|
|
126
|
+
pi,
|
|
127
|
+
dependencies.sourcePath
|
|
128
|
+
? { capabilities: dependencies.sourcePath }
|
|
129
|
+
: undefined,
|
|
130
|
+
);
|
|
131
|
+
reconcileDiscoveryGateway();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
pi.on("before_agent_start", (event) => {
|
|
135
|
+
if (requestsCapabilityGateway(event.prompt)) {
|
|
136
|
+
patchOwnedTools(pi, "capabilities", {
|
|
137
|
+
enable: OPENPI_TOOL_SURFACE.capabilities.entry,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const requested = capabilitiesRequestedByPrompt(event.prompt);
|
|
141
|
+
if (requested.length > 0) loadOpenPiCapabilities(pi, requested);
|
|
142
|
+
const guidance = skillGuidance(requested);
|
|
143
|
+
if (guidance) {
|
|
144
|
+
return {
|
|
145
|
+
message: {
|
|
146
|
+
customType: "openpi-capability-guidance",
|
|
147
|
+
content: guidance,
|
|
148
|
+
display: false,
|
|
149
|
+
details: { capabilities: requested },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
pi.registerTool({
|
|
156
|
+
name: "openpi_load_tools",
|
|
157
|
+
label: "Load OpenPI Tools",
|
|
158
|
+
description:
|
|
159
|
+
"Load optional OpenPI groups only when useful: search, delegate, workflow, background, or session. Ordinary file and shell work needs none. Loaded groups stay available.",
|
|
160
|
+
parameters: OpenPiLoadToolsParameters,
|
|
161
|
+
async execute(_toolCallId, params: OpenPiLoadToolsInput) {
|
|
162
|
+
const result = params.groups
|
|
163
|
+
? loadOpenPiCapabilities(pi, params.groups)
|
|
164
|
+
: {
|
|
165
|
+
newlyLoaded: [],
|
|
166
|
+
loaded: getLoadedOpenPiCapabilities(pi),
|
|
167
|
+
activatedTools: [],
|
|
168
|
+
};
|
|
169
|
+
const available = OPENPI_CAPABILITY_NAMES.map((name) => ({
|
|
170
|
+
name,
|
|
171
|
+
summary: OPENPI_CAPABILITY_GROUPS[name].summary,
|
|
172
|
+
loaded: result.loaded.includes(name),
|
|
173
|
+
}));
|
|
174
|
+
const text = params.groups
|
|
175
|
+
? result.newlyLoaded.length > 0
|
|
176
|
+
? `Loaded OpenPI capabilities: ${result.newlyLoaded.join(", ")}. Activated tools: ${result.activatedTools.join(", ") || "none yet"}.${skillGuidance(result.newlyLoaded) ? ` ${skillGuidance(result.newlyLoaded)}` : ""}`
|
|
177
|
+
: `Requested OpenPI capabilities were already loaded: ${result.loaded.join(", ") || "none"}.`
|
|
178
|
+
: `Available OpenPI capabilities:\n${available
|
|
179
|
+
.map(
|
|
180
|
+
({ name, summary, loaded }) =>
|
|
181
|
+
`- ${name}${loaded ? " (loaded)" : ""}: ${summary}`,
|
|
182
|
+
)
|
|
183
|
+
.join("\n")}`;
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text" as const, text }],
|
|
186
|
+
details: {
|
|
187
|
+
available,
|
|
188
|
+
loaded: result.loaded,
|
|
189
|
+
newlyLoaded: result.newlyLoaded,
|
|
190
|
+
activatedTools: result.activatedTools,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default createCapabilitiesExtension();
|
|
@@ -4,6 +4,10 @@ import type {
|
|
|
4
4
|
SessionEntry,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
+
import {
|
|
8
|
+
OPENPI_TOOL_SURFACE,
|
|
9
|
+
patchOwnedTools,
|
|
10
|
+
} from "../shared/tool-surface.ts";
|
|
7
11
|
|
|
8
12
|
export const MIN_CONTEXT_PIVOT_TOKENS = 30_000;
|
|
9
13
|
const STATUS_KEY = "context-pivot";
|
|
@@ -78,6 +82,22 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
78
82
|
let generation = 0;
|
|
79
83
|
let pending: PendingPivot | undefined;
|
|
80
84
|
let compacting = false;
|
|
85
|
+
const setVisibleForContext = (ctx?: ExtensionContext) => {
|
|
86
|
+
const tokens = ctx ? estimateContextTokens(ctx.getContextUsage()) : null;
|
|
87
|
+
patchOwnedTools(pi, "context", {
|
|
88
|
+
...(tokens !== null && tokens >= MIN_CONTEXT_PIVOT_TOKENS
|
|
89
|
+
? { enable: OPENPI_TOOL_SURFACE.context.deferred }
|
|
90
|
+
: { disable: OPENPI_TOOL_SURFACE.context.deferred }),
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pi.on("session_start", () => {
|
|
95
|
+
setVisibleForContext();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
99
|
+
setVisibleForContext(ctx);
|
|
100
|
+
});
|
|
81
101
|
|
|
82
102
|
pi.on("session_before_compact", (event) => {
|
|
83
103
|
const pivot = pending;
|
|
@@ -99,6 +119,7 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
99
119
|
generation += 1;
|
|
100
120
|
pending = undefined;
|
|
101
121
|
compacting = false;
|
|
122
|
+
setVisibleForContext();
|
|
102
123
|
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
103
124
|
});
|
|
104
125
|
|
package/extensions/cron/index.ts
CHANGED
|
@@ -17,10 +17,10 @@ import type {
|
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import {
|
|
19
19
|
advanceDeliveredJobs,
|
|
20
|
+
type CronJob,
|
|
20
21
|
dueJobs,
|
|
21
22
|
formatInterval,
|
|
22
23
|
parseCronCommand,
|
|
23
|
-
type CronJob,
|
|
24
24
|
} from "./schedule.ts";
|
|
25
25
|
|
|
26
26
|
/** How often the scheduler looks for due jobs. */
|
|
@@ -55,20 +55,47 @@ export default function cron(
|
|
|
55
55
|
stopPolling = undefined;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
const fire = (
|
|
58
|
+
const fire = (due: readonly CronJob[]) => {
|
|
59
|
+
if (due.length === 0) return true;
|
|
59
60
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
const jobs = due.map((job) => ({
|
|
62
|
+
id: job.id,
|
|
63
|
+
prompt: job.prompt,
|
|
64
|
+
recurring: job.intervalMs !== undefined,
|
|
65
|
+
}));
|
|
66
|
+
const message =
|
|
67
|
+
due.length === 1
|
|
68
|
+
? {
|
|
69
|
+
customType: "cron-fire",
|
|
70
|
+
content: `[cron ${jobs[0]!.id} · ${jobs[0]!.recurring ? "recurring" : "once"}]\n${jobs[0]!.prompt}`,
|
|
71
|
+
display: true,
|
|
72
|
+
details: jobs[0],
|
|
73
|
+
}
|
|
74
|
+
: {
|
|
75
|
+
customType: "cron-fire",
|
|
76
|
+
content: `${due.length} scheduled prompts are due:\n\n${jobs
|
|
77
|
+
.map(
|
|
78
|
+
(job) =>
|
|
79
|
+
`[cron ${job.id} · ${job.recurring ? "recurring" : "once"}]\n${job.prompt}`,
|
|
80
|
+
)
|
|
81
|
+
.join("\n\n")}`,
|
|
82
|
+
display: true,
|
|
83
|
+
details: { count: jobs.length, jobs },
|
|
84
|
+
};
|
|
85
|
+
pi.sendMessage<
|
|
86
|
+
| { id: number; prompt: string; recurring: boolean }
|
|
87
|
+
| {
|
|
88
|
+
count: number;
|
|
89
|
+
jobs: Array<{ id: number; prompt: string; recurring: boolean }>;
|
|
90
|
+
}
|
|
91
|
+
>(message, {
|
|
92
|
+
deliverAs: "followUp",
|
|
93
|
+
triggerTurn: true,
|
|
94
|
+
});
|
|
69
95
|
return true;
|
|
70
96
|
} catch {
|
|
71
|
-
// Session may be shutting down; leave the
|
|
97
|
+
// Session may be shutting down; leave the whole batch due for the next
|
|
98
|
+
// tick. A partial advance would silently lose prompts.
|
|
72
99
|
return false;
|
|
73
100
|
}
|
|
74
101
|
};
|
|
@@ -83,10 +110,10 @@ export default function cron(
|
|
|
83
110
|
const due = dueJobs(jobs, now);
|
|
84
111
|
if (due.length === 0) return;
|
|
85
112
|
const deliveredIds = new Set<number>();
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
if (fire(due)) {
|
|
114
|
+
for (const job of due) deliveredIds.add(job.id);
|
|
88
115
|
}
|
|
89
|
-
jobs = advanceDeliveredJobs(jobs, deliveredIds, now);
|
|
116
|
+
jobs = advanceDeliveredJobs(jobs, deliveredIds, runtime.now());
|
|
90
117
|
if (jobs.length === 0) stopTicker();
|
|
91
118
|
};
|
|
92
119
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ContextEvent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
type Message = ContextEvent["messages"][number];
|
|
5
|
+
|
|
6
|
+
export interface ActiveEvidencePolicy {
|
|
7
|
+
minimumTransactionsBeforeProjection: number;
|
|
8
|
+
minActiveTransactions: number;
|
|
9
|
+
epochStride: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_ACTIVE_EVIDENCE_POLICY: ActiveEvidencePolicy = {
|
|
13
|
+
minimumTransactionsBeforeProjection: 9,
|
|
14
|
+
minActiveTransactions: 3,
|
|
15
|
+
epochStride: 6,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DELAYED_ACTIVE_EVIDENCE_POLICY: ActiveEvidencePolicy = {
|
|
19
|
+
minimumTransactionsBeforeProjection: 15,
|
|
20
|
+
minActiveTransactions: 3,
|
|
21
|
+
epochStride: 6,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface Transaction {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
callIds: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function serializedChars(messages: readonly Message[]) {
|
|
31
|
+
return messages.reduce(
|
|
32
|
+
(total, message) => total + JSON.stringify(message).length,
|
|
33
|
+
0,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assistantToolCallIds(message: Message) {
|
|
38
|
+
if (message.role !== "assistant") return [];
|
|
39
|
+
return message.content.flatMap((block) =>
|
|
40
|
+
block.type === "toolCall" ? [block.id] : [],
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseTransactions(messages: readonly Message[], start: number) {
|
|
45
|
+
const transactions: Transaction[] = [];
|
|
46
|
+
let index = start;
|
|
47
|
+
while (index < messages.length) {
|
|
48
|
+
const assistant = messages[index];
|
|
49
|
+
if (!assistant) return undefined;
|
|
50
|
+
const callIds = assistantToolCallIds(assistant);
|
|
51
|
+
if (callIds.length === 0 || new Set(callIds).size !== callIds.length) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const expected = new Set(callIds);
|
|
56
|
+
const observed = new Set<string>();
|
|
57
|
+
let cursor = index + 1;
|
|
58
|
+
while (cursor < messages.length && observed.size < expected.size) {
|
|
59
|
+
const result = messages[cursor];
|
|
60
|
+
if (result?.role !== "toolResult") return undefined;
|
|
61
|
+
if (!expected.has(result.toolCallId) || observed.has(result.toolCallId)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
observed.add(result.toolCallId);
|
|
65
|
+
cursor += 1;
|
|
66
|
+
}
|
|
67
|
+
if (observed.size !== expected.size) return undefined;
|
|
68
|
+
|
|
69
|
+
transactions.push({ start: index, end: cursor, callIds });
|
|
70
|
+
index = cursor;
|
|
71
|
+
}
|
|
72
|
+
return transactions;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function latestUserIndex(messages: readonly Message[]) {
|
|
76
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
77
|
+
if (messages[index]?.role === "user") return index;
|
|
78
|
+
}
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function projectActiveEvidence(
|
|
83
|
+
messages: readonly Message[],
|
|
84
|
+
policy = DEFAULT_ACTIVE_EVIDENCE_POLICY,
|
|
85
|
+
) {
|
|
86
|
+
const userIndex = latestUserIndex(messages);
|
|
87
|
+
if (userIndex < 0 || userIndex === messages.length - 1) return undefined;
|
|
88
|
+
|
|
89
|
+
const transactions = parseTransactions(messages, userIndex + 1);
|
|
90
|
+
if (!transactions) return undefined;
|
|
91
|
+
if (transactions.length < policy.minimumTransactionsBeforeProjection) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const closable = transactions.length - policy.minActiveTransactions;
|
|
96
|
+
const closedTransactions =
|
|
97
|
+
Math.floor(closable / policy.epochStride) * policy.epochStride;
|
|
98
|
+
if (closedTransactions < policy.epochStride) return undefined;
|
|
99
|
+
|
|
100
|
+
const first = transactions[0];
|
|
101
|
+
const retained = transactions[closedTransactions];
|
|
102
|
+
if (!first || !retained) return undefined;
|
|
103
|
+
|
|
104
|
+
const closedMessages = messages.slice(first.start, retained.start);
|
|
105
|
+
const digest = createHash("sha256")
|
|
106
|
+
.update(JSON.stringify(closedMessages))
|
|
107
|
+
.digest("hex")
|
|
108
|
+
.slice(0, 16);
|
|
109
|
+
const epoch = closedTransactions / policy.epochStride;
|
|
110
|
+
const projectedMessages: Message[] = [
|
|
111
|
+
...messages.slice(0, first.start),
|
|
112
|
+
...messages.slice(retained.start),
|
|
113
|
+
];
|
|
114
|
+
const originalChars = serializedChars(messages);
|
|
115
|
+
const projectedChars = serializedChars(projectedMessages);
|
|
116
|
+
if (projectedChars >= originalChars) return undefined;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
messages: projectedMessages,
|
|
120
|
+
receipt: {
|
|
121
|
+
epoch,
|
|
122
|
+
closedTransactions,
|
|
123
|
+
retainedTransactions: transactions.length - closedTransactions,
|
|
124
|
+
originalChars,
|
|
125
|
+
projectedChars,
|
|
126
|
+
digest,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|