pi-maestro-teammate 2.8.0 → 2.9.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 +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +5 -5
- package/package.json +4 -3
- package/src/classify/client.ts +73 -12
- package/src/classify/engine.ts +108 -17
- package/src/extension/index.ts +27 -26
- package/src/extension/monitor.ts +2 -0
- package/src/extension/teammate-core.ts +0 -3
- package/src/extension/teammate-helpers.ts +2370 -2372
- package/src/extension/teammate-proxy.ts +3753 -3756
- package/src/public/v1/classify.ts +4 -0
- package/src/public/v1/events.ts +3 -0
- package/src/runs/execution-infra.ts +37 -8
- package/src/runs/host-observers.ts +36 -0
- package/src/runs/pi-subprocess-attempt.ts +61 -14
- package/src/runs/rpc-stdout-projection.mjs +51 -0
- package/src/transcript/session-history.ts +96 -18
- package/src/tui/render.ts +53 -45
- package/types/classify/client.d.ts +18 -1
- package/types/classify/engine.d.ts +14 -5
- package/types/public/v1/classify.d.ts +3 -3
- package/types/public/v1/events.d.ts +2 -0
- package/types/runs/execution-infra.d.ts +5 -1
- package/types/runs/host-observers.d.ts +21 -0
- package/types/transcript/session-history.d.ts +11 -1
- package/types/tui/render.d.ts +14 -11
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Pi extension for dispatching one or more role-based teammate tasks through a sin
|
|
|
6
6
|
|
|
7
7
|
## Breaking Changes In 1.0
|
|
8
8
|
|
|
9
|
-
> Current version: **2.8.0
|
|
9
|
+
> Current version: **2.8.1**, with `pi-maestro-backends@0.1.7`; Backend-Core 0.1.5 and Settings-Core 0.2.3 remain unchanged. Classifier budgets and diagnostics are session-scoped; child compaction preserves the original task, cumulative search budget, and exact result references. See [RELEASE.md](../../RELEASE.md) for dependency changes and verification limits. The 1.0 breaking changes below remain in effect; later releases added circuit breaker, retry resilience, quiet state, duration tracking, observe `watch`/`until=completed`, per-workspace mailbox isolation, lifecycle hardening, and explicit model-registry routing without breaking the v1 public import paths.
|
|
10
10
|
|
|
11
11
|
- Every public `teammate` call requires a non-empty `tasks` array.
|
|
12
12
|
- Single-agent work is represented by `tasks` with one item.
|
package/agents/explorer.md
CHANGED
|
@@ -31,7 +31,7 @@ From the dispatch prompt, extract:
|
|
|
31
31
|
3. **Verify** — read the strongest matches to confirm they answer the target; discard weak hits instead of reporting them.
|
|
32
32
|
4. **Report** — emit findings per the Output contract below.
|
|
33
33
|
|
|
34
|
-
Budget: at most two search rounds beyond the initial pass.
|
|
34
|
+
Budget: at most two search rounds beyond the initial pass, or the caller's tighter limit. A round is one bounded batch of queries for unresolved targets, not a separate allowance for each symbol or file. Track rounds cumulatively across compact/new_context; preserve consumed rounds, confirmed findings, negative results, and the exact next action in any handoff. Once the target is answered, return immediately. Do not re-search already-read files merely to polish line citations. At the limit, report what remains unresolved rather than widening endlessly.
|
|
35
35
|
|
|
36
36
|
## Output
|
|
37
37
|
|
package/agents/planner.md
CHANGED
|
@@ -33,9 +33,9 @@ Treat a matched template as governing evidence: cite its entry ID in `## Evidenc
|
|
|
33
33
|
## Process
|
|
34
34
|
|
|
35
35
|
1. **Research** — gather governing knowledge (`maestro search` → `maestro load`), verify repository facts yourself, and delegate at most one bounded nested task when it materially helps. Before designing, check for existing decision documents (`docs/*decision*`, `docs/*architecture*`, `maestro search "<topic> 决策|方案"`); when one exists, cite it in `## Evidence` and treat its locked decisions as settled premises — do not re-litigate them in the Plan.
|
|
36
|
-
2. **Design** —
|
|
37
|
-
3. **Compose** — write the Plan per the Document Contract below.
|
|
38
|
-
4. **Self-check** —
|
|
36
|
+
2. **Design** — select the smallest end-to-end path using existing authorities. Separate user-required outcomes and explicit human-locked safety/compatibility decisions from agent-selected implementation means; select one approach without treating those means as immutable human requirements.
|
|
37
|
+
3. **Compose** — write the Plan per the Document Contract below, proportionate to the requested outcome. Keep auxiliary diagnostics, provenance, and hardening out of the critical path unless a requirement or confirmed material defect makes them necessary.
|
|
38
|
+
4. **Self-check** — trace every user requirement to an outcome; justify each blocking dependency with its requirement or confirmed defect, not merely an enumerated risk. Make shared input/authority contracts available before consumers need production-path acceptance. Confirm complete coverage and an executable DAG; identify the first end-to-end acceptance milestone, cumulative correction budget, and when to report a simpler equivalent option or need for redesign/user guidance.
|
|
39
39
|
|
|
40
40
|
## Output — Document Contract
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ Return only Markdown for the Plan, with no preface, commentary, interview log, o
|
|
|
46
46
|
3. `## Evidence`: list governing knowledge or spec IDs, verified code entry points with `file:line` anchors, current behavior, and constraints. Separate verified facts from assumptions.
|
|
47
47
|
4. `## Scope`: list explicit in-scope and out-of-scope boundaries, including compatibility and migration constraints.
|
|
48
48
|
5. `## Requirements`: provide a table with `ID`, `Requirement / source`, `Planned outcome`, and `Acceptance evidence`. Map every user requirement to one or more planned outcomes.
|
|
49
|
-
6. `## Design`:
|
|
49
|
+
6. `## Design`: specify the selected implementation, affected interfaces and data flow, and error and failure behavior; label explicit human-locked decisions separately from revisable implementation means. Equivalent simplification must preserve required behavior and guarantees; changes to approved scope, explicit locked decisions, or safety boundaries require renewed approval, never silent gate removal. A Plan describes exactly one selected approach — never present multiple alternative approaches side by side with a recommendation. When alternatives and their trade-offs matter, reference the decision document that settled them in `## Evidence`, or move the unresolved choice to `## Open Decisions`; do not embed an option comparison in the Plan body.
|
|
50
50
|
7. `## Execution Plan`: define ordered, outcome-sized tasks. Every task must contain these fields:
|
|
51
51
|
- `ID`
|
|
52
52
|
- `Outcome`
|
|
@@ -57,7 +57,7 @@ Return only Markdown for the Plan, with no preface, commentary, interview log, o
|
|
|
57
57
|
- `Verification`
|
|
58
58
|
Dependencies must form an executable DAG and identify safe parallel work. A task is a verifiable outcome, not a command or activity log.
|
|
59
59
|
8. `## Validation`: specify exact commands or observable checks, expected results, requirement coverage, and relevant regression or integration boundaries.
|
|
60
|
-
9. `## Risks and Recovery`: state concrete risks, mitigations, and rollback or recovery behavior.
|
|
60
|
+
9. `## Risks and Recovery`: state concrete risks, mitigations, and rollback or recovery behavior. Risks are not automatically acceptance gates; justify any blocking mitigation. Count failed attempts by shared interface/state machine and delivery blocker across workers, task names, approaches, and resets; after three unsuccessful attempts, stop the correction chain and report its effect on the requested outcome plus a simpler equivalent option or need for redesign/user guidance.
|
|
61
61
|
10. `## Knowledge Outcome`: require an end-of-execution assessment after implementation and verification. Name plausible candidate topics only when the work may produce a reusable non-obvious pitfall, failure lesson, trade-off, or prescriptive constraint; otherwise write `None expected` with a concrete reason. This section predicts what to assess—it must not fabricate knowledge in advance. Actual staging happens after approval in Act/Run mode, and the executor must explicitly report zero candidates when no result meets the project's knowledge quality bar.
|
|
62
62
|
11. `## Open Decisions`: list unresolved user-owned decisions. Write `None` only after evidence-based review; a Plan with unresolved decisions is not confirmation-ready.
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "node --experimental-transform-types --import ./test/setup.ts --test --test-concurrency=2 \"test/*.test.ts\"",
|
|
24
|
+
"test:host-observers-packed": "node --test test/host-observers-packed.test.mjs",
|
|
24
25
|
"typecheck": "tsc -p tsconfig.build.json",
|
|
25
26
|
"build:declarations": "tsc -p tsconfig.declarations.json",
|
|
26
27
|
"check:declarations": "node scripts/check-declarations.mjs",
|
|
@@ -176,7 +177,7 @@
|
|
|
176
177
|
},
|
|
177
178
|
"files": [
|
|
178
179
|
"bin/*.mjs",
|
|
179
|
-
"src/**/*.{ts,json}",
|
|
180
|
+
"src/**/*.{ts,mjs,json}",
|
|
180
181
|
"types/**/*.d.ts",
|
|
181
182
|
"agents/",
|
|
182
183
|
"README.md"
|
|
@@ -216,7 +217,7 @@
|
|
|
216
217
|
"jiti": "2.7.0",
|
|
217
218
|
"pi-maestro-settings-core": "0.2.3",
|
|
218
219
|
"pi-maestro-backend-core": "0.1.5",
|
|
219
|
-
"pi-maestro-backends": "0.1.
|
|
220
|
+
"pi-maestro-backends": "0.1.7",
|
|
220
221
|
"pi-maestro-fabric-core": "0.1.0",
|
|
221
222
|
"proper-lockfile": "4.1.2",
|
|
222
223
|
"ssh2": "1.17.0",
|
package/src/classify/client.ts
CHANGED
|
@@ -56,9 +56,28 @@ export interface JevClient {
|
|
|
56
56
|
|
|
57
57
|
interface PreparedJevDecision {
|
|
58
58
|
identity: string;
|
|
59
|
+
effectiveModel?: string;
|
|
60
|
+
/** Present only after native authenticated model resolution. */
|
|
61
|
+
authenticatedModel?: string;
|
|
59
62
|
decide(): Promise<JevResponse>;
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
export interface ClassifierRuntimeReadiness {
|
|
66
|
+
status: "disabled" | "unknown" | "available" | "unavailable";
|
|
67
|
+
available: boolean;
|
|
68
|
+
reason?: string;
|
|
69
|
+
model?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const probes = new WeakMap<JevClient, (signal?: AbortSignal) => Promise<ClassifierRuntimeReadiness>>();
|
|
73
|
+
|
|
74
|
+
/** @internal Authentication-only probe; never submits a model request. */
|
|
75
|
+
export async function probeJevClient(client: JevClient, signal?: AbortSignal): Promise<ClassifierRuntimeReadiness> {
|
|
76
|
+
signal?.throwIfAborted();
|
|
77
|
+
const probe = probes.get(client);
|
|
78
|
+
return probe ? probe(signal) : { status: "unknown", available: false, reason: "Legacy classifier credentials configured but not verified" };
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// Keep preparation internal to these clients: cache identity and execution must
|
|
63
82
|
// use the same resolved native model, even when the host registry changes.
|
|
64
83
|
const preparations = new WeakMap<JevClient, (request: Omit<JevRequest, "model">) => Promise<PreparedJevDecision>>();
|
|
@@ -136,19 +155,55 @@ export interface ClassifierRuntime {
|
|
|
136
155
|
classify(model: ClassifierModel<ClassifierApi>, context: ClassifierContext, options?: ModelsClassifierOptions): Promise<ClassifierResult>;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
export
|
|
158
|
+
export interface NativeJevClientOptions {
|
|
159
|
+
/** Absent means auto across authenticated classifier providers. */
|
|
160
|
+
endpoint?: JevEndpoint;
|
|
161
|
+
model?: string;
|
|
162
|
+
timeoutMs?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function nativeSignal(options: NativeJevClientOptions, signal?: AbortSignal): AbortSignal {
|
|
166
|
+
const timeout = AbortSignal.timeout(Math.max(1, options.timeoutMs ?? JEV_DEFAULT_TIMEOUT_MS));
|
|
167
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** One resolver for both execution and readiness; registry entries alone are not auth evidence. */
|
|
171
|
+
async function resolveNativeClassifierModel(runtime: ClassifierRuntime, options: NativeJevClientOptions, signal: AbortSignal): Promise<ClassifierModel<ClassifierApi>> {
|
|
172
|
+
signal.throwIfAborted();
|
|
173
|
+
const reference = options.model?.trim() || undefined;
|
|
174
|
+
const slash = reference?.indexOf("/") ?? -1;
|
|
175
|
+
const pinnedProvider = slash > 0 ? reference!.slice(0, slash) : undefined;
|
|
176
|
+
if (options.endpoint && pinnedProvider && pinnedProvider !== options.endpoint) {
|
|
177
|
+
throw new Error("Native classifier unavailable (model provider conflicts with explicit endpoint)");
|
|
178
|
+
}
|
|
179
|
+
const provider = options.endpoint ?? pinnedProvider;
|
|
180
|
+
let abort!: () => void;
|
|
181
|
+
const cancelled = new Promise<never>((_resolve, reject) => {
|
|
182
|
+
abort = () => reject(signal.reason ?? new Error("Classifier probe aborted"));
|
|
183
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
184
|
+
if (signal.aborted) abort();
|
|
185
|
+
});
|
|
186
|
+
let available: readonly ClassifierModel<ClassifierApi>[];
|
|
187
|
+
try {
|
|
188
|
+
available = await Promise.race([runtime.getAvailableOfType("classifier", provider, { signal }), cancelled]);
|
|
189
|
+
signal.throwIfAborted();
|
|
190
|
+
} finally {
|
|
191
|
+
signal.removeEventListener("abort", abort);
|
|
192
|
+
}
|
|
193
|
+
const candidates = available.filter((entry) => !provider || entry.provider === provider);
|
|
194
|
+
// Stable preference within provider tiers: never manufacture a default model.
|
|
195
|
+
const rank = (entry: ClassifierModel<ClassifierApi>) => entry.provider === "typesafe" ? 0 : entry.provider === "openrouter" ? 1 : 2;
|
|
196
|
+
const model = reference
|
|
197
|
+
? candidates.find((entry) => slash > 0 ? entry.provider === pinnedProvider && entry.id === reference.slice(slash + 1) : entry.id === reference)
|
|
198
|
+
: candidates.reduce<ClassifierModel<ClassifierApi> | undefined>((best, entry) => !best || rank(entry) < rank(best) ? entry : best, undefined);
|
|
199
|
+
if (!model) throw new Error("Native classifier unavailable (no matching authenticated classifier model)");
|
|
200
|
+
return model;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function createNativeJevClient(runtime: ClassifierRuntime, options: NativeJevClientOptions = {}): JevClient {
|
|
140
204
|
const prepare = async (request: Omit<JevRequest, "model">): Promise<PreparedJevDecision> => {
|
|
141
|
-
const signal =
|
|
142
|
-
const
|
|
143
|
-
const available = await runtime.getAvailableOfType("classifier", options.model?.includes("/") ? undefined : provider, { signal });
|
|
144
|
-
const reference = options.model;
|
|
145
|
-
const slash = reference?.indexOf("/") ?? -1;
|
|
146
|
-
const model = reference
|
|
147
|
-
? (slash > 0
|
|
148
|
-
? runtime.getModelOfType("classifier", reference.slice(0, slash), reference.slice(slash + 1))
|
|
149
|
-
: available.find((entry) => entry.id === reference))
|
|
150
|
-
: available[0];
|
|
151
|
-
if (!model) throw new Error("Native classifier unavailable (no matching authenticated classifier model)");
|
|
205
|
+
const signal = nativeSignal(options);
|
|
206
|
+
const model = await resolveNativeClassifierModel(runtime, options, signal);
|
|
152
207
|
const questions: ClassifierContext["questions"] = {};
|
|
153
208
|
for (const [id, question] of Object.entries(request.questions)) {
|
|
154
209
|
questions[id] = question.type === "noul"
|
|
@@ -159,6 +214,7 @@ export function createNativeJevClient(runtime: ClassifierRuntime, options: Pick<
|
|
|
159
214
|
}
|
|
160
215
|
return {
|
|
161
216
|
identity: JSON.stringify(["native", options.endpoint, model.provider, model.api, model.baseUrl, model.id]),
|
|
217
|
+
authenticatedModel: `${model.provider}/${model.id}`,
|
|
162
218
|
async decide() {
|
|
163
219
|
const result = await runtime.classify(model, { state: { text: request.state }, questions }, { signal });
|
|
164
220
|
if (result.stopReason !== "stop") throw new Error(result.errorMessage ?? `Native classifier ${result.stopReason}`);
|
|
@@ -174,6 +230,10 @@ export function createNativeJevClient(runtime: ClassifierRuntime, options: Pick<
|
|
|
174
230
|
};
|
|
175
231
|
const client: JevClient = { decide: async (request) => (await prepare(request)).decide() };
|
|
176
232
|
preparations.set(client, prepare);
|
|
233
|
+
probes.set(client, async (parent) => {
|
|
234
|
+
const model = await resolveNativeClassifierModel(runtime, options, nativeSignal(options, parent));
|
|
235
|
+
return { status: "available", available: true, model: `${model.provider}/${model.id}` };
|
|
236
|
+
});
|
|
177
237
|
return client;
|
|
178
238
|
}
|
|
179
239
|
|
|
@@ -204,6 +264,7 @@ export function createJevClient(options: JevClientOptions): JevClient {
|
|
|
204
264
|
};
|
|
205
265
|
preparations.set(client, async (request) => ({
|
|
206
266
|
identity: JSON.stringify(["http", options.endpoint, url, model]),
|
|
267
|
+
effectiveModel: model,
|
|
207
268
|
decide: () => client.decide(request),
|
|
208
269
|
}));
|
|
209
270
|
return client;
|
package/src/classify/engine.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
createJevClient,
|
|
30
30
|
createNativeJevClient,
|
|
31
31
|
type ClassifierRuntime,
|
|
32
|
+
type ClassifierRuntimeReadiness,
|
|
33
|
+
probeJevClient,
|
|
32
34
|
prepareJevDecision,
|
|
33
35
|
resolveJevEndpoint,
|
|
34
36
|
type JevClient,
|
|
@@ -61,7 +63,7 @@ export interface ClassifierConfig {
|
|
|
61
63
|
timeoutMs?: number;
|
|
62
64
|
/** Answer cache TTL (default 10min — error text repeats heavily). */
|
|
63
65
|
cacheTtlMs?: number;
|
|
64
|
-
/** Max JEV calls per
|
|
66
|
+
/** Max JEV calls per process-local session + cwd identity (default 30). */
|
|
65
67
|
maxCallsPerSession?: number;
|
|
66
68
|
/** domain name → mode. Absent = "off". */
|
|
67
69
|
domains?: Record<string, ClassifierDomainMode>;
|
|
@@ -81,6 +83,9 @@ export interface ClassifierStatus {
|
|
|
81
83
|
endpoint?: JevEndpoint;
|
|
82
84
|
apiKeyPresent: boolean;
|
|
83
85
|
model?: string;
|
|
86
|
+
runtimeStatus?: ClassifierRuntimeReadiness["status"];
|
|
87
|
+
runtimeReason?: string;
|
|
88
|
+
effectiveModel?: string;
|
|
84
89
|
callsUsed: number;
|
|
85
90
|
maxCalls: number;
|
|
86
91
|
cacheSize: number;
|
|
@@ -92,9 +97,22 @@ const DEFAULT_MAX_CALLS = 30;
|
|
|
92
97
|
|
|
93
98
|
let config: ClassifierConfig = { enabled: false };
|
|
94
99
|
let client: JevClient | undefined;
|
|
95
|
-
|
|
100
|
+
export interface ClassifierRuntimeBinding {
|
|
101
|
+
hostVersion: string;
|
|
102
|
+
runtime: ClassifierRuntime;
|
|
103
|
+
sessionId?: string;
|
|
104
|
+
cwd?: string;
|
|
105
|
+
}
|
|
106
|
+
export type { ClassifierRuntimeReadiness } from "./client.ts";
|
|
107
|
+
|
|
108
|
+
let hostBinding: ClassifierRuntimeBinding | undefined;
|
|
96
109
|
let unavailableReason = "Classifier host runtime unavailable";
|
|
97
|
-
let
|
|
110
|
+
let apiKeyPresent = false;
|
|
111
|
+
let readiness: ClassifierRuntimeReadiness = { status: "disabled", available: false };
|
|
112
|
+
let readinessSequence = 0;
|
|
113
|
+
// Quota survives lifecycle fencing; intentionally no disk persistence.
|
|
114
|
+
let budgetKey = JSON.stringify(["", ""]);
|
|
115
|
+
const budgets = new Map<string, number>();
|
|
98
116
|
const registry = new Map<string, ClassifyDomain<string, unknown>>();
|
|
99
117
|
const cache = new Map<string, { response: JevResponse; at: number }>();
|
|
100
118
|
const pending = new Map<string, Promise<JevResponse>>();
|
|
@@ -103,13 +121,13 @@ let generation = {};
|
|
|
103
121
|
|
|
104
122
|
function fenceGeneration(): void {
|
|
105
123
|
generation = {};
|
|
106
|
-
|
|
124
|
+
readinessSequence++;
|
|
107
125
|
cache.clear();
|
|
108
126
|
pending.clear();
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
function captureGeneration() {
|
|
112
|
-
return { generation, config, client, unavailableReason };
|
|
130
|
+
return { generation, config, client, unavailableReason, budgetKey, legacy: apiKeyPresent };
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
type Generation = ReturnType<typeof captureGeneration>;
|
|
@@ -119,8 +137,9 @@ function assertCurrent(owner: Generation): void {
|
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
/** Bind only the current process's host facade, never a child/remote runtime. */
|
|
122
|
-
export function bindClassifierRuntime(binding:
|
|
123
|
-
hostBinding = binding;
|
|
140
|
+
export function bindClassifierRuntime(binding: ClassifierRuntimeBinding): void {
|
|
141
|
+
hostBinding = { ...binding };
|
|
142
|
+
budgetKey = JSON.stringify([binding.sessionId ?? "", binding.cwd ?? ""]);
|
|
124
143
|
configureClassifier(config);
|
|
125
144
|
}
|
|
126
145
|
|
|
@@ -130,6 +149,8 @@ export function unbindClassifierRuntime(runtime: ClassifierRuntime): void {
|
|
|
130
149
|
hostBinding = undefined;
|
|
131
150
|
client = undefined;
|
|
132
151
|
unavailableReason = "Classifier host runtime unavailable";
|
|
152
|
+
apiKeyPresent = false;
|
|
153
|
+
readiness = config.enabled ? { status: "unavailable", available: false, reason: unavailableReason } : { status: "disabled", available: false };
|
|
133
154
|
}
|
|
134
155
|
|
|
135
156
|
/** Inject classifier configuration (host loads `.pi/classifier.json` / env). */
|
|
@@ -139,10 +160,13 @@ export function configureClassifier(next: ClassifierConfig): void {
|
|
|
139
160
|
const runtime = hostBinding?.runtime;
|
|
140
161
|
const owner = getPiFeatureOwner(hostBinding?.hostVersion ?? next.hostVersion, !!runtime && typeof runtime.classify === "function" && typeof runtime.getAvailableOfType === "function" && typeof runtime.getModelOfType === "function");
|
|
141
162
|
client = undefined;
|
|
163
|
+
apiKeyPresent = false;
|
|
142
164
|
unavailableReason = "Classifier host runtime unavailable";
|
|
165
|
+
readiness = config.enabled ? { status: "unavailable", available: false, reason: unavailableReason } : { status: "disabled", available: false };
|
|
143
166
|
if (!config.enabled) return;
|
|
144
167
|
if (owner === "native" && runtime) {
|
|
145
|
-
client = createNativeJevClient(runtime, { endpoint: next.endpoint
|
|
168
|
+
client = createNativeJevClient(runtime, { endpoint: next.endpoint, model: next.model, timeoutMs: next.timeoutMs });
|
|
169
|
+
readiness = { status: "unknown", available: false, reason: "Native classifier authentication has not been probed" };
|
|
146
170
|
return;
|
|
147
171
|
}
|
|
148
172
|
if (owner !== "legacy") return;
|
|
@@ -160,6 +184,33 @@ export function configureClassifier(next: ClassifierConfig): void {
|
|
|
160
184
|
...(next.fetchFn ? { fetchFn: next.fetchFn } : {}),
|
|
161
185
|
} as JevClientOptions)
|
|
162
186
|
: undefined;
|
|
187
|
+
apiKeyPresent = !!resolved;
|
|
188
|
+
readiness = resolved
|
|
189
|
+
? { status: "unknown", available: false, reason: "Legacy classifier credentials configured but not verified" }
|
|
190
|
+
: { status: "unavailable", available: false, reason: unavailableReason };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Refresh authentication/model readiness without spending model-call quota. */
|
|
194
|
+
export async function probeClassifierRuntime(signal?: AbortSignal): Promise<ClassifierRuntimeReadiness> {
|
|
195
|
+
const owner = captureGeneration();
|
|
196
|
+
// Legacy probes do not authenticate, so they cannot supersede request evidence.
|
|
197
|
+
const sequence = owner.legacy ? readinessSequence : ++readinessSequence;
|
|
198
|
+
if (!owner.config.enabled || !owner.client) return { ...readiness };
|
|
199
|
+
let result: ClassifierRuntimeReadiness;
|
|
200
|
+
try {
|
|
201
|
+
result = await probeJevClient(owner.client, signal);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
result = { status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error) };
|
|
204
|
+
}
|
|
205
|
+
if (owner.generation !== generation) {
|
|
206
|
+
return { status: "unknown", available: false, reason: "Classifier readiness superseded by runtime/config/session generation" };
|
|
207
|
+
}
|
|
208
|
+
// A non-verifying legacy probe must preserve same-generation execution evidence.
|
|
209
|
+
if (owner.legacy && result.status === "unknown") result = { ...readiness };
|
|
210
|
+
// Concurrent same-generation consumers may use their own auth result, but
|
|
211
|
+
// only the newest refresh owns the synchronous status projection.
|
|
212
|
+
if (sequence === readinessSequence && !signal?.aborted) readiness = result;
|
|
213
|
+
return { ...result };
|
|
163
214
|
}
|
|
164
215
|
|
|
165
216
|
export function classifierConfig(): ClassifierConfig {
|
|
@@ -173,6 +224,10 @@ export function resetClassifierForTest(): void {
|
|
|
173
224
|
client = undefined;
|
|
174
225
|
hostBinding = undefined;
|
|
175
226
|
unavailableReason = "Classifier host runtime unavailable";
|
|
227
|
+
apiKeyPresent = false;
|
|
228
|
+
readiness = { status: "disabled", available: false };
|
|
229
|
+
budgets.clear();
|
|
230
|
+
budgetKey = JSON.stringify(["", ""]);
|
|
176
231
|
registry.clear();
|
|
177
232
|
}
|
|
178
233
|
|
|
@@ -196,9 +251,12 @@ export function classifierStatus(): ClassifierStatus {
|
|
|
196
251
|
return {
|
|
197
252
|
enabled: config.enabled === true,
|
|
198
253
|
...(config.endpoint ? { endpoint: config.endpoint } : {}),
|
|
199
|
-
apiKeyPresent
|
|
254
|
+
apiKeyPresent,
|
|
200
255
|
...(config.model ? { model: config.model } : {}),
|
|
201
|
-
|
|
256
|
+
runtimeStatus: readiness.status,
|
|
257
|
+
...(readiness.reason ? { runtimeReason: readiness.reason } : {}),
|
|
258
|
+
...(readiness.model ? { effectiveModel: readiness.model } : {}),
|
|
259
|
+
callsUsed: budgets.get(budgetKey) ?? 0,
|
|
202
260
|
maxCalls: config.maxCallsPerSession ?? DEFAULT_MAX_CALLS,
|
|
203
261
|
cacheSize: cache.size,
|
|
204
262
|
domains,
|
|
@@ -245,26 +303,56 @@ async function jevDecide<D extends string, I>(
|
|
|
245
303
|
assertCurrent(owner);
|
|
246
304
|
if (!owner.client) throw new Error(owner.unavailableReason);
|
|
247
305
|
const questions = domain.questions();
|
|
248
|
-
|
|
306
|
+
// Native preparation is auth evidence. Legacy owns status only after cache,
|
|
307
|
+
// pending or quota admission; a rejected request must not supersede execution.
|
|
308
|
+
let sequence = owner.legacy ? undefined : ++readinessSequence;
|
|
309
|
+
let prepared: Awaited<ReturnType<typeof prepareJevDecision>>;
|
|
310
|
+
try {
|
|
311
|
+
prepared = await prepareJevDecision(owner.client, { state, questions });
|
|
312
|
+
} catch (error) {
|
|
313
|
+
assertCurrent(owner);
|
|
314
|
+
if (sequence === readinessSequence) readiness = { status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error) };
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
249
317
|
assertCurrent(owner);
|
|
318
|
+
if (prepared.authenticatedModel && sequence === readinessSequence) {
|
|
319
|
+
readiness = { status: "available", available: true, model: prepared.authenticatedModel };
|
|
320
|
+
}
|
|
250
321
|
const key = createHash("sha256")
|
|
251
322
|
.update(JSON.stringify([domain.name, prepared.identity, questions, state]))
|
|
252
323
|
.digest("hex");
|
|
324
|
+
const publishSuccess = (response: JevResponse): JevResponse => {
|
|
325
|
+
assertCurrent(owner);
|
|
326
|
+
if (sequence === readinessSequence) readiness = {
|
|
327
|
+
status: "available", available: true,
|
|
328
|
+
model: prepared.authenticatedModel ?? response.model ?? prepared.effectiveModel,
|
|
329
|
+
};
|
|
330
|
+
return response;
|
|
331
|
+
};
|
|
332
|
+
const publishFailure = (error: unknown): void => {
|
|
333
|
+
if (owner.generation === generation && sequence === readinessSequence) readiness = {
|
|
334
|
+
status: "unavailable", available: false, reason: error instanceof Error ? error.message : String(error),
|
|
335
|
+
...(prepared.authenticatedModel ? { model: prepared.authenticatedModel } : {}),
|
|
336
|
+
};
|
|
337
|
+
};
|
|
253
338
|
const entry = cache.get(key);
|
|
254
339
|
if (entry && Date.now() - entry.at <= (owner.config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS)) {
|
|
255
|
-
|
|
340
|
+
sequence ??= ++readinessSequence;
|
|
341
|
+
return publishSuccess(entry.response);
|
|
256
342
|
}
|
|
257
343
|
cache.delete(key);
|
|
258
344
|
const existing = pending.get(key);
|
|
259
345
|
if (existing) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
346
|
+
sequence ??= ++readinessSequence;
|
|
347
|
+
try { return publishSuccess(await existing); }
|
|
348
|
+
catch (error) { publishFailure(error); throw error; }
|
|
263
349
|
}
|
|
350
|
+
const callsUsed = budgets.get(owner.budgetKey) ?? 0;
|
|
264
351
|
if (callsUsed >= (owner.config.maxCallsPerSession ?? DEFAULT_MAX_CALLS)) {
|
|
265
352
|
throw new Error("JEV session call budget exhausted");
|
|
266
353
|
}
|
|
267
|
-
callsUsed
|
|
354
|
+
budgets.set(owner.budgetKey, callsUsed + 1);
|
|
355
|
+
sequence ??= ++readinessSequence;
|
|
268
356
|
// Reserve before invoking host code, which may re-enter configuration.
|
|
269
357
|
const decision = Promise.resolve().then(() => {
|
|
270
358
|
assertCurrent(owner);
|
|
@@ -275,7 +363,10 @@ async function jevDecide<D extends string, I>(
|
|
|
275
363
|
const response = await decision;
|
|
276
364
|
assertCurrent(owner);
|
|
277
365
|
cache.set(key, { response, at: Date.now() });
|
|
278
|
-
return response;
|
|
366
|
+
return publishSuccess(response);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
publishFailure(error);
|
|
369
|
+
throw error;
|
|
279
370
|
} finally {
|
|
280
371
|
// A late old request must not remove a new generation's reservation.
|
|
281
372
|
if (owner.generation === generation && pending.get(key) === decision) pending.delete(key);
|
package/src/extension/index.ts
CHANGED
|
@@ -271,8 +271,7 @@ import type {
|
|
|
271
271
|
} from "../runs/execution.ts";
|
|
272
272
|
import { createForkSnapshot } from "../runs/fork-snapshot.ts";
|
|
273
273
|
import {
|
|
274
|
-
|
|
275
|
-
auxToolResultFallback,
|
|
274
|
+
auxToolResultCard,
|
|
276
275
|
renderCompletionOutboxMessage,
|
|
277
276
|
renderMonitorResult,
|
|
278
277
|
renderTeammateCompletionFallbackMessage,
|
|
@@ -280,13 +279,14 @@ import {
|
|
|
280
279
|
renderTeammateStalledMessage,
|
|
281
280
|
renderObserveCall,
|
|
282
281
|
renderObserveResult,
|
|
283
|
-
|
|
282
|
+
teammateCallLine,
|
|
284
283
|
renderTeammateCall,
|
|
285
284
|
renderTeammateListCall,
|
|
286
285
|
renderTeammateListResult,
|
|
287
286
|
renderTeammateResult,
|
|
288
287
|
renderTeammateSendCall,
|
|
289
288
|
renderTeammateSendResult,
|
|
289
|
+
teammateStatusRow,
|
|
290
290
|
} from "../tui/render.ts";
|
|
291
291
|
import { AttachOverlay } from "../tui/attach-overlay.ts";
|
|
292
292
|
import {
|
|
@@ -482,7 +482,7 @@ import {
|
|
|
482
482
|
registerTeammateChildProxyCaller,
|
|
483
483
|
resolvedRunLocation,
|
|
484
484
|
} from "../runs/child-extensions.ts";
|
|
485
|
-
import { setQuietMode } from "pi-maestro-settings-core/ui";
|
|
485
|
+
import { isQuietMode, setQuietMode } from "pi-maestro-settings-core/ui";
|
|
486
486
|
import {
|
|
487
487
|
aggregateAgentRunPhase,
|
|
488
488
|
diagnoseAgentRuntime,
|
|
@@ -743,7 +743,7 @@ export default function registerTeammateExtension(
|
|
|
743
743
|
const content = typeof message.content === "string"
|
|
744
744
|
? message.content.replace(/^●\s*/, "")
|
|
745
745
|
: "agent spawned";
|
|
746
|
-
return
|
|
746
|
+
return teammateStatusRow("teammate-started", content, "success", theme as ExtensionContext["ui"]["theme"]);
|
|
747
747
|
},
|
|
748
748
|
);
|
|
749
749
|
|
|
@@ -842,7 +842,8 @@ export default function registerTeammateExtension(
|
|
|
842
842
|
let classifierRuntime: ExtensionContext["modelRegistry"] | undefined;
|
|
843
843
|
pi.on("session_start", (_event, ctx) => {
|
|
844
844
|
classifierRuntime = ctx.modelRegistry;
|
|
845
|
-
bindClassifierRuntime({ hostVersion: PI_VERSION, runtime: classifierRuntime
|
|
845
|
+
bindClassifierRuntime({ hostVersion: PI_VERSION, runtime: classifierRuntime,
|
|
846
|
+
sessionId: ctx.sessionManager.getSessionId(), cwd: ctx.cwd });
|
|
846
847
|
});
|
|
847
848
|
pi.on("session_shutdown", () => {
|
|
848
849
|
if (classifierRuntime) unbindClassifierRuntime(classifierRuntime);
|
|
@@ -6928,15 +6929,15 @@ export default function registerTeammateExtension(
|
|
|
6928
6929
|
renderCall(args, theme, context) {
|
|
6929
6930
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
6930
6931
|
const lines = typeof args.lines === "number" ? ` · ${args.lines} lines` : "";
|
|
6931
|
-
return
|
|
6932
|
-
?? auxToolCallFallback("teammate-watch", theme);
|
|
6932
|
+
return teammateCallLine(theme, "teammate-watch", `@${String(args.name ?? "?")}${lines}`);
|
|
6933
6933
|
},
|
|
6934
6934
|
|
|
6935
6935
|
renderResult(result, options, theme, context) {
|
|
6936
6936
|
if (options.isPartial) return new Text("", 0, 0);
|
|
6937
6937
|
const failed = context.isError || (result as { isError?: boolean }).isError === true;
|
|
6938
|
-
return
|
|
6939
|
-
|
|
6938
|
+
return isQuietMode()
|
|
6939
|
+
? teammateStatusRow("teammate-watch", failed ? "inspection failed" : "inspected", failed ? "failure" : "success", theme)
|
|
6940
|
+
: auxToolResultCard("teammate-watch", result, theme, options);
|
|
6940
6941
|
},
|
|
6941
6942
|
};
|
|
6942
6943
|
|
|
@@ -6983,16 +6984,16 @@ export default function registerTeammateExtension(
|
|
|
6983
6984
|
renderCall(args, theme, context) {
|
|
6984
6985
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
6985
6986
|
const target = args.name ? `@${String(args.name)}` : `${String(args.waitMs ?? 0)}ms`;
|
|
6986
|
-
return
|
|
6987
|
-
?? auxToolCallFallback("teammate-wait", theme);
|
|
6987
|
+
return teammateCallLine(theme, "teammate-wait", target);
|
|
6988
6988
|
},
|
|
6989
6989
|
|
|
6990
6990
|
renderResult(result, options, theme, context) {
|
|
6991
6991
|
if (options.isPartial) return new Text("", 0, 0);
|
|
6992
6992
|
const status = result.details?.status ?? "timeout";
|
|
6993
6993
|
const failed = context.isError || (result as { isError?: boolean }).isError === true;
|
|
6994
|
-
return
|
|
6995
|
-
|
|
6994
|
+
return isQuietMode()
|
|
6995
|
+
? teammateStatusRow("teammate-wait", status, failed ? "failure" : "success", theme)
|
|
6996
|
+
: auxToolResultCard("teammate-wait", result, theme, options);
|
|
6996
6997
|
},
|
|
6997
6998
|
};
|
|
6998
6999
|
|
|
@@ -9637,9 +9638,9 @@ Use list for an attention-first overview, get for one complete normalized window
|
|
|
9637
9638
|
details: query,
|
|
9638
9639
|
};
|
|
9639
9640
|
},
|
|
9640
|
-
renderCall(_args, theme, context) {
|
|
9641
|
+
renderCall(_args: Record<string, unknown>, theme, context) {
|
|
9641
9642
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
9642
|
-
return
|
|
9643
|
+
return teammateCallLine(theme, "monitor", String(_args.action ?? "list"));
|
|
9643
9644
|
},
|
|
9644
9645
|
renderResult(result, options, theme, context) {
|
|
9645
9646
|
return renderMonitorResult(result, options, theme, context.isError);
|
|
@@ -9720,15 +9721,15 @@ Use list for an attention-first overview, get for one complete normalized window
|
|
|
9720
9721
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
9721
9722
|
const action = String(args.action ?? "status");
|
|
9722
9723
|
const targets = Array.isArray(args.targets) ? (args.targets as string[]).join(", ") : "";
|
|
9723
|
-
return
|
|
9724
|
-
?? auxToolCallFallback("teammate-monitor", theme);
|
|
9724
|
+
return teammateCallLine(theme, "teammate-monitor", `${action} ${targets}`);
|
|
9725
9725
|
},
|
|
9726
9726
|
|
|
9727
9727
|
renderResult(result, options, theme, context) {
|
|
9728
9728
|
if (options.isPartial) return new Text("", 0, 0);
|
|
9729
9729
|
const failed = context.isError || (result as { isError?: boolean }).isError === true;
|
|
9730
|
-
return
|
|
9731
|
-
|
|
9730
|
+
return isQuietMode()
|
|
9731
|
+
? teammateStatusRow("teammate-monitor", failed ? "failed" : "ok", failed ? "failure" : "success", theme)
|
|
9732
|
+
: auxToolResultCard("teammate-monitor", result, theme, options);
|
|
9732
9733
|
},
|
|
9733
9734
|
};
|
|
9734
9735
|
|
|
@@ -9950,13 +9951,13 @@ This lifecycle tool is available only after the user enters Monitor mode with /m
|
|
|
9950
9951
|
created.handle,
|
|
9951
9952
|
);
|
|
9952
9953
|
},
|
|
9953
|
-
renderCall(_args, theme, context) {
|
|
9954
|
+
renderCall(_args: Record<string, unknown>, theme, context) {
|
|
9954
9955
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
9955
|
-
return
|
|
9956
|
+
return teammateCallLine(theme, "workspace-window", String(_args.action ?? ""));
|
|
9956
9957
|
},
|
|
9957
9958
|
renderResult(result, options, theme) {
|
|
9958
9959
|
if (options.isPartial) return new Text("", 0, 0);
|
|
9959
|
-
return
|
|
9960
|
+
return auxToolResultCard("workspace-window", result, theme, options);
|
|
9960
9961
|
},
|
|
9961
9962
|
};
|
|
9962
9963
|
|
|
@@ -10057,13 +10058,13 @@ This Monitor-only lifecycle tool loads configured target ids without exposing SS
|
|
|
10057
10058
|
return result(sanitizeRemoteMonitorError(error, "worker operation"), true);
|
|
10058
10059
|
}
|
|
10059
10060
|
},
|
|
10060
|
-
renderCall(_args, theme, context) {
|
|
10061
|
+
renderCall(_args: Record<string, unknown>, theme, context) {
|
|
10061
10062
|
if (context.isPartial === false) return new Text("", 0, 0);
|
|
10062
|
-
return
|
|
10063
|
+
return teammateCallLine(theme, "remote-worker", String(_args.action ?? ""));
|
|
10063
10064
|
},
|
|
10064
10065
|
renderResult(result, options, theme) {
|
|
10065
10066
|
if (options.isPartial) return new Text("", 0, 0);
|
|
10066
|
-
return
|
|
10067
|
+
return auxToolResultCard("remote-worker", result, theme, options);
|
|
10067
10068
|
},
|
|
10068
10069
|
};
|
|
10069
10070
|
|
package/src/extension/monitor.ts
CHANGED
|
@@ -76,6 +76,8 @@ const MONITOR_MODE_CONTEXT = [
|
|
|
76
76
|
"## Complete-project orchestration",
|
|
77
77
|
"For a complete software project, begin with monitor list and relevant lifecycle list tools to inventory existing work and reuse suitable workers. Never dispatch an undivided project objective directly to implementation. If no decision-complete plan exists, create one read-only planning or technical-lead worker to inspect project knowledge and current code and return requirements, architecture, interfaces, risks, acceptance criteria, and a dependency-aware phase plan. Read its exact completion resource, then bring only genuine product or architecture decisions to the user.",
|
|
78
78
|
"After decisions are locked, execute a phase DAG. Every worker objective must state purpose, owned scope, dependencies, locked interfaces, acceptance criteria, focused verification, and required result resources. Create only runnable workers, parallelize only independent scopes, and keep each shared interface or state machine under one owner until stable. Use explicit phase barriers for dependencies between different windows; never infer dependency completion from timing or message delivery.",
|
|
79
|
+
"Prioritize the smallest end-to-end path to the user-required outcome using existing authorities. Separate explicit locked constraints from agent-selected implementation means; justify every blocking edge with a requirement or confirmed material defect. Auxiliary diagnostics, provenance, or hardening do not automatically gate unrelated delivery. Make shared authorities available before consumers need production-path acceptance; reconcile an unnecessary gate with the root/user instead of bypassing it, and obtain renewed approval for scope, explicit locked-decision, or safety-boundary changes.",
|
|
80
|
+
"Track failed attempts cumulatively by shared interface/state machine and delivery blocker across workers, task names, approaches, and resets. After three unsuccessful attempts, stop the correction chain and report its effect on the requested outcome, evidence, and a simpler equivalent option or need for redesign/user guidance. Reuse an existing execution worker for bounded corrections; do not create a new worker merely to reset the budget. If direct execution is needed, ask the user to exit Monitor first; this control window remains supervision-only.",
|
|
79
81
|
"At a phase barrier, establish lifecycle state with monitor or observe as appropriate, then read exact completion resources and correlated Flow reports before accepting work; settled windows alone do not prove project success. Reuse still-valid verification evidence, diagnose failed or ambiguous work before retrying, and dispatch integration, review, or verification only after its dependencies pass. The project is ready only when integrated behavior and acceptance criteria are verified. Release or deployment is not implied by implementation: it requires explicit user authorization and a dedicated worker.",
|
|
80
82
|
"## Tool routing",
|
|
81
83
|
"Use workspace-window only for local Pi workers. Create only a required worker; native interactive is the default. provider=herdr requires an already-running local Herdr session, supports interactive presentation only, and never starts or stops the Herdr server. Create already delivers the objective, so do not resend it. Retain the returned exact owner target and completion handle, read settled results through its immutable agent:// resource, and close only windows created by this Monitor; never close discovered external peers.",
|