@awebai/oats-pi 0.35.3 → 0.35.5
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 +8 -0
- package/extension/index.ts +58 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ from the same release tag). This bridge registers no operational tools. It only:
|
|
|
10
10
|
acquisition funnel);
|
|
11
11
|
- contributes the instance-local `.agents/skills` set inside a spawned
|
|
12
12
|
instance;
|
|
13
|
+
- holds an instance's opening task (`@TASK.md`) on pi's own path until no
|
|
14
|
+
turn another extension started is active, so pi runs it exactly once,
|
|
15
|
+
as its input processing made it, instead of refusing it. It never sends,
|
|
16
|
+
re-sends or alters a message. The exception: an extension loaded behind
|
|
17
|
+
the bridge that starts a turn (or awaits I/O while one starts) inside its
|
|
18
|
+
own input or before_agent_start handling of the opening prompt can still
|
|
19
|
+
make pi refuse it, because pi's interactive mode sends the initial
|
|
20
|
+
message with no streamingBehavior. @awebai/pi has no such handler;
|
|
13
21
|
- journals compaction summaries and sends resume nudges when the active
|
|
14
22
|
knowledge capability created `STATE.md`/`log.md`. Knowledge ownership,
|
|
15
23
|
read/capture instructions, judgment and delivery remain capability-owned.
|
package/extension/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* pre-workspace oats-getting-started bootstrap outside one, and drives the
|
|
7
7
|
* memory session events. Ambient skills coexist with the OATS-composed set.
|
|
8
8
|
*/
|
|
9
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { appendLogEntry, PACKAGED_SKILLS_DIR } from "./core-loader.mjs";
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
@@ -25,6 +25,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
if (isInstance) {
|
|
28
|
+
holdOpeningTask(pi);
|
|
29
|
+
|
|
28
30
|
pi.on("session_compact", async (event) => {
|
|
29
31
|
if (!existsSync(join(agentHome!, "STATE.md"))) return;
|
|
30
32
|
try {
|
|
@@ -57,3 +59,58 @@ export default function (pi: ExtensionAPI) {
|
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The opening task runs once, after any turn another extension starts, as
|
|
65
|
+
* pi's input processing made it. pi sends the launch's `@TASK.md` as the
|
|
66
|
+
* session's first prompt with no streamingBehavior (pi 0.85.1
|
|
67
|
+
* modes/interactive/interactive-mode.js:816) and refuses it while another
|
|
68
|
+
* extension's run (the @awebai/pi welcome) is active. The bridge never sends
|
|
69
|
+
* or alters a message: it holds that prompt on pi's own path, in its input
|
|
70
|
+
* handler and again in before_agent_start (a run can start during the
|
|
71
|
+
* prompt's preflight), until no run is active. A run that an extension behind
|
|
72
|
+
* the bridge starts (or that starts while one awaits) in its own input or
|
|
73
|
+
* before_agent_start handling of the prompt can still make pi refuse it, and
|
|
74
|
+
* pi says nothing an extension could act on. Every later prompt is left to pi.
|
|
75
|
+
*/
|
|
76
|
+
function holdOpeningTask(pi: ExtensionAPI) {
|
|
77
|
+
let taken = false;
|
|
78
|
+
// The opening prompt has yet to reach before_agent_start.
|
|
79
|
+
let awaitingStart = false;
|
|
80
|
+
const waiters: (() => void)[] = [];
|
|
81
|
+
// pi refuses a prompt while pi-agent-core holds a run (ctx.signal is that
|
|
82
|
+
// run's), and one with no streamingBehavior while the session is not idle.
|
|
83
|
+
const busy = (ctx: ExtensionContext) => ctx.signal !== undefined || !ctx.isIdle();
|
|
84
|
+
const untilFree = async (ctx: ExtensionContext) => {
|
|
85
|
+
while (busy(ctx)) await new Promise<void>((resolve) => waiters.push(resolve));
|
|
86
|
+
};
|
|
87
|
+
const wake = () => { for (const resume of waiters.splice(0)) resume(); };
|
|
88
|
+
// Waiters re-check on agent_settled (pi 0.80.4 and later). A pi without it
|
|
89
|
+
// releases a run only once every agent_end listener has finished, which no
|
|
90
|
+
// event marks: from agent_end, waiters re-check every 25 ms until none is
|
|
91
|
+
// left waiting or another run starts.
|
|
92
|
+
let recheck: ReturnType<typeof setTimeout> | undefined;
|
|
93
|
+
const stopRechecking = () => { clearTimeout(recheck); recheck = undefined; };
|
|
94
|
+
const recheckWaiters = () => {
|
|
95
|
+
recheck = undefined;
|
|
96
|
+
if (!waiters.length) return;
|
|
97
|
+
wake();
|
|
98
|
+
recheck = setTimeout(recheckWaiters, 25);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
pi.on("input", async (event, ctx) => {
|
|
102
|
+
if (taken || event.source !== "interactive" || event.streamingBehavior !== undefined) return { action: "continue" };
|
|
103
|
+
taken = true;
|
|
104
|
+
awaitingStart = true;
|
|
105
|
+
await untilFree(ctx);
|
|
106
|
+
return { action: "continue" };
|
|
107
|
+
});
|
|
108
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
109
|
+
if (!awaitingStart) return;
|
|
110
|
+
awaitingStart = false;
|
|
111
|
+
await untilFree(ctx);
|
|
112
|
+
});
|
|
113
|
+
pi.on("agent_start", stopRechecking);
|
|
114
|
+
pi.on("agent_end", () => { stopRechecking(); recheck = setTimeout(recheckWaiters, 0); });
|
|
115
|
+
pi.on("agent_settled", () => { stopRechecking(); wake(); });
|
|
116
|
+
}
|
package/package.json
CHANGED