@jwilger/pi-development-system 0.39.0 → 0.41.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/package.json
CHANGED
|
@@ -15,10 +15,15 @@ export function registerCompactionResync(deps: { pi: ExtensionAPI; state: Sessio
|
|
|
15
15
|
deps.pi.on("session_compact", () => {
|
|
16
16
|
const state = deps.state.get();
|
|
17
17
|
if (state.phase === "idle" && state.openDepartures.length === 0) return;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// triggerTurn:false: while a run is active pi would otherwise steer the message, and the queued
|
|
19
|
+
// message would force an extra model turn after automatic compaction.
|
|
20
|
+
deps.pi.sendMessage(
|
|
21
|
+
{
|
|
22
|
+
customType: RESYNC_ENTRY_TYPE,
|
|
23
|
+
content: `[devsys resync] Context was compacted. Current working state:\n\n${renderStateBlock(state)}`,
|
|
24
|
+
display: true,
|
|
25
|
+
},
|
|
26
|
+
{ triggerTurn: false },
|
|
27
|
+
);
|
|
23
28
|
});
|
|
24
29
|
}
|
|
@@ -39,7 +39,10 @@ export function registerModelAdvice(deps: ModelAdviceDeps): void {
|
|
|
39
39
|
});
|
|
40
40
|
if (advice === undefined) return;
|
|
41
41
|
told.add(key);
|
|
42
|
-
deps.pi.sendMessage(
|
|
42
|
+
deps.pi.sendMessage(
|
|
43
|
+
{ customType: ADVICE_ENTRY_TYPE, content: advice, display: true },
|
|
44
|
+
{ triggerTurn: false },
|
|
45
|
+
);
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
deps.pi.on("session_start", (_event, ctx) => {
|
|
@@ -32,13 +32,14 @@ export function assistantParts(message: AgentMessage): Parts | undefined {
|
|
|
32
32
|
const LIST_ITEM = /^(?:\d+[.)]|[-*+])\s/;
|
|
33
33
|
|
|
34
34
|
const QUESTION_END = /\?(?:\s*\([^()]{0,40}\))?[\s)"'*_`]*$/;
|
|
35
|
-
/** A lead-in that
|
|
36
|
-
const
|
|
35
|
+
/** A lead-in that asks for a choice ("Want me to:", "Which next:"), not a status summary ("Changes:"). */
|
|
36
|
+
const CHOICE_LEAD_IN =
|
|
37
|
+
/\b(?:want me to|would you like|should i|shall i|which|do you want|let me know|choose|pick)\b[^\n]*:\s*$/i;
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* True when the message ends by asking the user something: the last non-empty line, or the last line
|
|
40
41
|
* before an option list, ends with a question mark (optionally followed by a short parenthetical
|
|
41
|
-
* like "(y/n)"), or a bare option list follows a
|
|
42
|
+
* like "(y/n)"), or a bare option list follows a lead-in that asks for a choice. A plain "Summary:" list is not a question.
|
|
42
43
|
*/
|
|
43
44
|
export function asksUser(text: string): boolean {
|
|
44
45
|
const lines = text
|
|
@@ -50,7 +51,7 @@ export function asksUser(text: string): boolean {
|
|
|
50
51
|
if (QUESTION_END.test(last)) return true;
|
|
51
52
|
const lead = lines.filter((l) => !LIST_ITEM.test(l)).at(-1);
|
|
52
53
|
if (lead === undefined) return false;
|
|
53
|
-
return QUESTION_END.test(lead) || (LIST_ITEM.test(last) &&
|
|
54
|
+
return QUESTION_END.test(lead) || (LIST_ITEM.test(last) && CHOICE_LEAD_IN.test(lead));
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/** True when a scope.expansion departure already covers the slice: the remedy the note asks for exists. */
|
|
@@ -40,3 +40,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
40
40
|
SOFTWARE.
|
|
41
41
|
6. Empty scope (I5 review): `orch/runtime.ts` `availableScopedModels` treats an empty `/scoped-models` as no restriction (pi's meaning) instead of "matches nothing", because every devsys agent declares `models:` and would otherwise be unspawnable by default. Pinned models are also re-applied on resume (`resolveInitialSettings`), since a restored session may hold only inherited history.
|
|
42
42
|
7. Bundled agents outside `src/subagents`: `agents/researcher.md` (devsys `models:` family list) and `agents/reviewer.md` (devsys `models:` list and packet format; description rewritten; upstream `icon` and `modelSuggestions` removed) differ from upstream; `agents/{advisor,implementer,lens-*}.md` are new. The agent file format itself is unchanged.
|
|
43
|
+
8. Thread cap (I7b.1): `orch/manager.ts` `archiveFinished` replaces the hard "Total thread limit reached" check. At `maxThreads` the oldest finished (completed/failed/stopped) thread without a retained descendant is dropped from the registry to make room; the limit is refused only when every kept thread is live, paused or a parent of one. Upstream kept finished threads forever, so long autonomous runs stalled on their own history. A dropped thread cannot be resumed with `agent_steer` (its session file stays on disk).
|
|
@@ -257,8 +257,7 @@ export class ThreadManager {
|
|
|
257
257
|
: level > maxLevels
|
|
258
258
|
)
|
|
259
259
|
throw new Error("Agent depth limit reached");
|
|
260
|
-
|
|
261
|
-
throw new Error("Total thread limit reached");
|
|
260
|
+
this.archiveFinished(this.options.maxThreads ?? DEFAULT_MANAGER_SETTINGS.maxThreads);
|
|
262
261
|
this.assertCapacity();
|
|
263
262
|
const type = structuredClone(this.options.getType(args.type));
|
|
264
263
|
if (overrides !== undefined) type.spawnOverrides = { ...overrides };
|
|
@@ -761,6 +760,24 @@ export class ThreadManager {
|
|
|
761
760
|
}
|
|
762
761
|
}
|
|
763
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* devsys: a finished thread must never block new work, or long autonomous runs stall
|
|
765
|
+
* on their own history. Make room for one more thread by dropping the oldest finished
|
|
766
|
+
* thread that has no retained descendant; live and paused threads are never dropped.
|
|
767
|
+
* Throws only when every retained thread is still live, paused or a parent of one.
|
|
768
|
+
*/
|
|
769
|
+
private archiveFinished(limit: number): void {
|
|
770
|
+
while (this.records.size >= limit) {
|
|
771
|
+
const paths = [...this.records.keys()];
|
|
772
|
+
const victim = paths.find((path) => {
|
|
773
|
+
const state = this.records.get(path)?.view.state;
|
|
774
|
+
const finished = state === "completed" || state === "failed" || state === "stopped";
|
|
775
|
+
return finished && !paths.some((other) => isDescendant(other, path));
|
|
776
|
+
});
|
|
777
|
+
if (victim === undefined) throw new Error("Total thread limit reached");
|
|
778
|
+
this.records.delete(victim);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
764
781
|
private assertCapacity(): void {
|
|
765
782
|
if (
|
|
766
783
|
[...this.records.values()].filter((record) => active(record.view)).length >=
|
|
@@ -34,7 +34,7 @@ const FIELDS = [
|
|
|
34
34
|
{
|
|
35
35
|
id: "maxThreads",
|
|
36
36
|
label: "Retained threads",
|
|
37
|
-
help: "How many agent sessions can be kept for follow-up work, including completed and paused agents.\nAt the limit
|
|
37
|
+
help: "How many agent sessions can be kept for follow-up work, including completed and paused agents.\nAt the limit the oldest finished agent is dropped to make room; new agents are refused only when every kept agent is still active, paused or has a kept child.\nPositive safe integer.",
|
|
38
38
|
},
|
|
39
39
|
] as const;
|
|
40
40
|
|