@dorokuma/herdsman-pi 0.14.9 → 0.15.1
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 +6 -6
- package/src/index.ts +38 -2
- package/src/wake.ts +41 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dorokuma/herdsman-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Pi extension bridge for Herdsman agent history.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"url": "git+https://github.com/dorokuma/herdsman.git",
|
|
22
22
|
"directory": "packages/herdsman-pi"
|
|
23
23
|
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
26
|
+
},
|
|
24
27
|
"pi": {
|
|
25
28
|
"extensions": [
|
|
26
29
|
"./src/index.ts"
|
|
@@ -34,8 +37,5 @@
|
|
|
34
37
|
"bugs": {
|
|
35
38
|
"url": "https://github.com/dorokuma/herdsman/issues"
|
|
36
39
|
},
|
|
37
|
-
"homepage": "https://github.com/dorokuma/herdsman#readme"
|
|
38
|
-
|
|
39
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
40
|
+
"homepage": "https://github.com/dorokuma/herdsman#readme"
|
|
41
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -97,6 +97,7 @@ type DeliveredBatch = {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
type HerdsmanState = {
|
|
100
|
+
autoClaimAttempted: boolean;
|
|
100
101
|
client: HerdsmanDaemonClient | undefined;
|
|
101
102
|
connected: boolean;
|
|
102
103
|
currentScope: CurrentScope | undefined;
|
|
@@ -450,6 +451,7 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
450
451
|
const wakeFilter = options.wakeFilter ?? loadWakeFilterConfig();
|
|
451
452
|
|
|
452
453
|
const state: HerdsmanState = {
|
|
454
|
+
autoClaimAttempted: false,
|
|
453
455
|
client: undefined,
|
|
454
456
|
connected: false,
|
|
455
457
|
currentScope: undefined,
|
|
@@ -2021,10 +2023,32 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
2021
2023
|
subscriberKind: "pi",
|
|
2022
2024
|
workspaceId: state.currentScope?.workspaceId ?? launchIdentity.workspaceId,
|
|
2023
2025
|
})
|
|
2024
|
-
.then((
|
|
2026
|
+
.then(async (rawResponse) => {
|
|
2025
2027
|
state.connected = true;
|
|
2026
2028
|
startKeepalive(client);
|
|
2027
|
-
|
|
2029
|
+
let response = rawResponse as ConnectionStateResponse;
|
|
2030
|
+
const currentPaneId = state.currentScope?.paneId ?? launchIdentity.paneId;
|
|
2031
|
+
if (
|
|
2032
|
+
process.env.HERDSMAN_AUTO_ON === "1" &&
|
|
2033
|
+
!state.autoClaimAttempted &&
|
|
2034
|
+
isMainOrchestratorPane(currentPaneId) &&
|
|
2035
|
+
!isDispatchedRoleSession(sessionRef.value, process.env)
|
|
2036
|
+
) {
|
|
2037
|
+
state.autoClaimAttempted = true;
|
|
2038
|
+
if (!isLocalOwner(response) && !response.state?.owner) {
|
|
2039
|
+
try {
|
|
2040
|
+
state.roleMutationInFlight = true;
|
|
2041
|
+
response = (await client.request("agent.orchestrator.set", {
|
|
2042
|
+
enabled: true,
|
|
2043
|
+
})) as ConnectionStateResponse;
|
|
2044
|
+
} catch {
|
|
2045
|
+
// Ignore race if another terminal claimed the workspace concurrently.
|
|
2046
|
+
} finally {
|
|
2047
|
+
state.roleMutationInFlight = false;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
applyConnectionStateResponse(response, ctx, {
|
|
2028
2052
|
notifyReconnectLoss: true,
|
|
2029
2053
|
});
|
|
2030
2054
|
})
|
|
@@ -2095,6 +2119,7 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
2095
2119
|
|
|
2096
2120
|
pi.on("session_start", (_event: unknown, ctx: PiContext) => {
|
|
2097
2121
|
activeContext = ctx;
|
|
2122
|
+
state.autoClaimAttempted = false;
|
|
2098
2123
|
state.subscriberId = ctx.sessionManager.getSessionId();
|
|
2099
2124
|
state.sessionRef = {
|
|
2100
2125
|
agent: "pi",
|
|
@@ -2438,6 +2463,17 @@ function herdrLaunchIdentity(environment: NodeJS.ProcessEnv): LaunchIdentity | u
|
|
|
2438
2463
|
return { herdrSocketPath, paneId, workspaceId };
|
|
2439
2464
|
}
|
|
2440
2465
|
|
|
2466
|
+
function isMainOrchestratorPane(paneId: string): boolean {
|
|
2467
|
+
return paneId.trim().endsWith(":p1");
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function isDispatchedRoleSession(sessionPath: string, environment: NodeJS.ProcessEnv): boolean {
|
|
2471
|
+
if (typeof environment.CTXMODE_ROLE === "string" && environment.CTXMODE_ROLE.trim().length > 0) {
|
|
2472
|
+
return true;
|
|
2473
|
+
}
|
|
2474
|
+
return sessionPath.startsWith("/tmp/herdr-role-sessions/");
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2441
2477
|
|
|
2442
2478
|
function stringValue(value: unknown): string | null {
|
|
2443
2479
|
return typeof value === "string" && value.length > 0 ? value : null;
|
package/src/wake.ts
CHANGED
|
@@ -7,15 +7,19 @@ import { DEFAULT_WAKE_FILTER_CONFIG, isUpstreamModelError, type WakeFilterConfig
|
|
|
7
7
|
// next microtask (0ms timer) instead of waiting out a settle window. Delivery
|
|
8
8
|
// latency is owned by the bounded deferral in the extension, not by this delay.
|
|
9
9
|
export const WAKE_SETTLE_MS = 0;
|
|
10
|
-
// Mirrors `
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// Mirrors the `STALLED_PLAN_ERRORS` set in `src/observability/contracts.ts`
|
|
11
|
+
// (`PLAN_WAITING_HISTORY` / `PLAN_STALLED` / `DEGRADED_PLAN_ERROR`, the last one
|
|
12
|
+
// spelled `"degraded"` inline below) and the copy in `src/agy/hook.ts`: the
|
|
13
|
+
// string value is the contract, and the reason is duplicated here because
|
|
14
|
+
// `@dorokuma/herdsman-pi` ships standalone `src/` without root `src/`. All three
|
|
15
|
+
// places must change together.
|
|
13
16
|
export const PLAN_WAITING_HISTORY = "PLAN_WAITING_HISTORY";
|
|
17
|
+
export const PLAN_STALLED = "PLAN_STALLED";
|
|
14
18
|
|
|
15
19
|
export type AgentOutcome = {
|
|
16
20
|
agent: string;
|
|
17
21
|
eventId: number;
|
|
18
|
-
kind: "blocked" | "completed" | "failed";
|
|
22
|
+
kind: "blocked" | "completed" | "failed" | "stalled";
|
|
19
23
|
name?: string | null;
|
|
20
24
|
paneId: string | null;
|
|
21
25
|
reason?: string;
|
|
@@ -93,8 +97,31 @@ function outcomeKind(event: AgentEventWireRecord): AgentOutcome["kind"] | undefi
|
|
|
93
97
|
return "failed";
|
|
94
98
|
}
|
|
95
99
|
if (event.type === "agent.discarded") {
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const payload = asRecord(event.payload);
|
|
101
|
+
const agent = stringValue(payload.agent);
|
|
102
|
+
const isAgyOrEmpty = agent === undefined || agent === "agy";
|
|
103
|
+
if (isAgyOrEmpty) {
|
|
104
|
+
const reason = stringValue(payload.reason);
|
|
105
|
+
// A stalled plan has *settled*, which makes it unlike PLAN_WAITING_HISTORY
|
|
106
|
+
// / degraded ("still waiting, nothing happened yet"): the round's outcome is
|
|
107
|
+
// gone and the orchestrator has to see that. With the fallback marker it
|
|
108
|
+
// projects as the self-readable `stalled`; a legacy row written before the
|
|
109
|
+
// marker existed still wakes as a plain `failed` rather than vanishing.
|
|
110
|
+
// A `pi` discard never enters this branch (`agent === "pi"` fails
|
|
111
|
+
// `isAgyOrEmpty`), so pi's own PLAN_STALLED handling is unchanged.
|
|
112
|
+
if (reason === PLAN_STALLED) {
|
|
113
|
+
if (payload.fallbackOutcome === true) {
|
|
114
|
+
return "stalled";
|
|
115
|
+
}
|
|
116
|
+
return "failed";
|
|
117
|
+
}
|
|
118
|
+
if (reason === PLAN_WAITING_HISTORY || reason === "degraded") {
|
|
119
|
+
if (payload.fallbackOutcome === true) {
|
|
120
|
+
return "stalled";
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
98
125
|
return "failed";
|
|
99
126
|
}
|
|
100
127
|
const payload = asRecord(event.payload);
|
|
@@ -169,7 +196,8 @@ function project(
|
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
const text = normalizeExcerpt(event.compactHistory?.lastAssistantMessage?.text);
|
|
172
|
-
const reason =
|
|
199
|
+
const reason =
|
|
200
|
+
kind === "failed" || kind === "stalled" ? normalizeExcerpt(payload.reason) : undefined;
|
|
173
201
|
// Upstream model errors are transient provider failures, not agent results:
|
|
174
202
|
// they are dropped from the wake projection without an outcome (and without
|
|
175
203
|
// being consumed into `seen`, so they stay visible as raw evidence).
|
|
@@ -243,6 +271,12 @@ export function formatAgentOutcomeUpdates(outcomes: AgentOutcome[]): string {
|
|
|
243
271
|
outcome.text.length > 0 ? `\n last assistant (pre-round): ${outcome.text}` : "";
|
|
244
272
|
return `- failed ${identity} ${pane}\n reason: ${reason}${body}`;
|
|
245
273
|
}
|
|
274
|
+
if (outcome.kind === "stalled") {
|
|
275
|
+
const reason = outcome.reason && outcome.reason.length > 0 ? outcome.reason : "(unknown)";
|
|
276
|
+
const body =
|
|
277
|
+
outcome.text.length > 0 ? `\n last assistant (pre-round): ${outcome.text}` : "";
|
|
278
|
+
return `- stalled ${identity} ${pane}\n reason: ${reason} (结果未取到,需自读 pane)${body}`;
|
|
279
|
+
}
|
|
246
280
|
const excerpt = outcome.text.length > 0 ? outcome.text : "(no assistant message)";
|
|
247
281
|
return `- ${outcome.kind} ${identity} ${pane}\n last assistant: ${excerpt}\n event: ${outcome.eventId}`;
|
|
248
282
|
}).join("\n");
|