@henryqw/pi-subagent 27.4.4 → 27.4.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/dist/direct-herdr.d.ts +2 -0
- package/dist/direct-herdr.js +116 -81
- package/package.json +1 -1
package/dist/direct-herdr.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare function directSessionUsage(jsonl: string, prompt: string): {
|
|
|
32
32
|
};
|
|
33
33
|
/** The last `maxChars` of assistant text in the exact prompt's turn; ignores a partial trailing entry. */
|
|
34
34
|
export declare function directPartialText(jsonl: string, prompt: string, maxChars: number): string | undefined;
|
|
35
|
+
/** One reader per handle; only complete, valid lines advance the byte offset. */
|
|
36
|
+
export declare function createDirectSessionReader(sessionFile: string, prompt: string): Pick<DirectHandle, "usageTokens" | "partialText" | "activity">;
|
|
35
37
|
/** `turn` is the prompt's random turn identity; it proves which session turn a record owns.
|
|
36
38
|
* `workspaceId` and `cwd` bind the record to its launching Herdr workspace and checkout. */
|
|
37
39
|
export type DirectTab = Pick<DirectHandle, "name" | "tabId" | "paneId" | "sessionFile"> & {
|
package/dist/direct-herdr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile, mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { open, readFile, mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
@@ -122,99 +122,150 @@ export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT, requir
|
|
|
122
122
|
throw new Error(`Direct task reported ${completion.outcome}: ${completion.answer}`);
|
|
123
123
|
return completion.answer;
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
class DirectSessionState {
|
|
126
|
+
active = false;
|
|
127
|
+
total = 0;
|
|
128
|
+
observed = false;
|
|
129
|
+
pending = new Map();
|
|
130
|
+
latest;
|
|
131
|
+
texts = [];
|
|
132
|
+
prompt;
|
|
133
|
+
constructor(prompt) { this.prompt = prompt; }
|
|
134
|
+
consume(line) {
|
|
133
135
|
if (!line.trim())
|
|
134
|
-
|
|
136
|
+
return true;
|
|
135
137
|
let entry;
|
|
136
138
|
try {
|
|
137
139
|
entry = JSON.parse(line);
|
|
138
140
|
}
|
|
139
141
|
catch {
|
|
140
|
-
|
|
142
|
+
return false;
|
|
141
143
|
}
|
|
142
144
|
if (!entry || entry.type !== "message" || !entry.message || typeof entry.message !== "object")
|
|
143
|
-
|
|
145
|
+
return true;
|
|
144
146
|
const message = entry.message;
|
|
145
147
|
if (message.role === "user") {
|
|
146
|
-
active = Array.isArray(message.content) && message.content.length === 1
|
|
147
|
-
&& message.content[0]?.text === prompt;
|
|
148
|
-
if (active) {
|
|
149
|
-
total = 0;
|
|
150
|
-
observed = false;
|
|
151
|
-
pending.clear();
|
|
152
|
-
latest = undefined;
|
|
148
|
+
this.active = Array.isArray(message.content) && message.content.length === 1
|
|
149
|
+
&& message.content[0]?.text === this.prompt;
|
|
150
|
+
if (this.active) {
|
|
151
|
+
this.total = 0;
|
|
152
|
+
this.observed = false;
|
|
153
|
+
this.pending.clear();
|
|
154
|
+
this.latest = undefined;
|
|
155
|
+
this.texts = [];
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
|
-
else if (active && message.role === "assistant") {
|
|
158
|
+
else if (this.active && message.role === "assistant") {
|
|
156
159
|
if (message.usage && typeof message.usage === "object") {
|
|
157
160
|
const usage = message.usage;
|
|
158
161
|
const values = [usage.input, usage.output];
|
|
159
162
|
if (values.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
|
|
160
|
-
total += values.reduce((sum, value) => sum + value, 0);
|
|
161
|
-
observed = true;
|
|
163
|
+
this.total += values.reduce((sum, value) => sum + value, 0);
|
|
164
|
+
this.observed = true;
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
if (Array.isArray(message.content))
|
|
165
168
|
for (const part of message.content) {
|
|
166
169
|
if (part?.type === "toolCall" && typeof part.id === "string" && typeof part.name === "string") {
|
|
167
|
-
pending.set(part.id, part.name);
|
|
170
|
+
this.pending.set(part.id, part.name);
|
|
171
|
+
}
|
|
172
|
+
else if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
173
|
+
this.texts.push(part.text);
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
|
-
else if (active && message.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
172
|
-
const name = pending.get(message.toolCallId);
|
|
173
|
-
if (name
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
else if (this.active && message.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
178
|
+
const name = this.pending.get(message.toolCallId);
|
|
179
|
+
if (name !== undefined) {
|
|
180
|
+
this.pending.delete(message.toolCallId);
|
|
181
|
+
this.latest = { name };
|
|
182
|
+
if (typeof message.durationMs === "number" && Number.isFinite(message.durationMs) && message.durationMs >= 0) {
|
|
183
|
+
this.latest.durationMs = message.durationMs;
|
|
184
|
+
}
|
|
179
185
|
}
|
|
180
186
|
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
usage() {
|
|
190
|
+
return { tokens: this.observed ? this.total : undefined,
|
|
191
|
+
activity: this.pending.size || this.latest ? { pending: [...this.pending.values()], latest: this.latest } : undefined };
|
|
192
|
+
}
|
|
193
|
+
partialText(maxChars) {
|
|
194
|
+
const chars = Array.from(this.texts.join("\n").trim());
|
|
195
|
+
if (!chars.length)
|
|
196
|
+
return undefined;
|
|
197
|
+
return chars.length <= maxChars ? chars.join("") : `…${chars.slice(-maxChars).join("")}`;
|
|
181
198
|
}
|
|
182
|
-
|
|
183
|
-
|
|
199
|
+
}
|
|
200
|
+
function parseDirectSession(jsonl, prompt) {
|
|
201
|
+
const state = new DirectSessionState(prompt);
|
|
202
|
+
for (const line of jsonl.split("\n"))
|
|
203
|
+
if (!state.consume(line))
|
|
204
|
+
break;
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
207
|
+
// Ignore a partial trailing JSONL entry while the child is writing it.
|
|
208
|
+
export function directSessionUsage(jsonl, prompt) {
|
|
209
|
+
return parseDirectSession(jsonl, prompt).usage();
|
|
184
210
|
}
|
|
185
211
|
/** The last `maxChars` of assistant text in the exact prompt's turn; ignores a partial trailing entry. */
|
|
186
212
|
export function directPartialText(jsonl, prompt, maxChars) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
return parseDirectSession(jsonl, prompt).partialText(maxChars);
|
|
214
|
+
}
|
|
215
|
+
/** One reader per handle; only complete, valid lines advance the byte offset. */
|
|
216
|
+
export function createDirectSessionReader(sessionFile, prompt) {
|
|
217
|
+
let offset = 0;
|
|
218
|
+
let previous;
|
|
219
|
+
let state = new DirectSessionState(prompt);
|
|
220
|
+
let lastUsage = {};
|
|
221
|
+
let reading;
|
|
222
|
+
const read = async () => {
|
|
223
|
+
const file = await open(sessionFile, "r");
|
|
193
224
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
225
|
+
const info = await file.stat();
|
|
226
|
+
if (info.size > SESSION_LIMIT) {
|
|
227
|
+
previous = info;
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (previous && (info.dev !== previous.dev || info.ino !== previous.ino || info.size < previous.size
|
|
231
|
+
|| (info.size === previous.size && (info.mtimeMs !== previous.mtimeMs || info.ctimeMs !== previous.ctimeMs)))) {
|
|
232
|
+
offset = 0;
|
|
233
|
+
state = new DirectSessionState(prompt);
|
|
234
|
+
}
|
|
235
|
+
previous = info;
|
|
236
|
+
const buffer = Buffer.alloc(info.size - offset);
|
|
237
|
+
let length = 0;
|
|
238
|
+
while (length < buffer.length) {
|
|
239
|
+
const { bytesRead } = await file.read({ buffer, offset: length, length: buffer.length - length, position: offset + length });
|
|
240
|
+
if (!bytesRead)
|
|
241
|
+
break;
|
|
242
|
+
length += bytesRead;
|
|
243
|
+
}
|
|
244
|
+
let start = 0;
|
|
245
|
+
for (let end = buffer.indexOf(10); end >= 0 && end < length; end = buffer.indexOf(10, start)) {
|
|
246
|
+
if (!state.consume(buffer.toString("utf8", start, end)))
|
|
247
|
+
break;
|
|
248
|
+
offset += end + 1 - start;
|
|
249
|
+
start = end + 1;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
207
252
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (part?.type === "text" && typeof part.text === "string" && part.text.trim())
|
|
211
|
-
texts.push(part.text);
|
|
253
|
+
finally {
|
|
254
|
+
await file.close();
|
|
212
255
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
256
|
+
};
|
|
257
|
+
// Timer samples and failure evidence can request the same file at the same time.
|
|
258
|
+
const sample = () => reading ??= read().finally(() => { reading = undefined; });
|
|
259
|
+
return {
|
|
260
|
+
get activity() { return lastUsage.activity; },
|
|
261
|
+
async usageTokens() {
|
|
262
|
+
lastUsage = await sample() ? state.usage() : {};
|
|
263
|
+
return lastUsage.tokens;
|
|
264
|
+
},
|
|
265
|
+
async partialText(maxChars) {
|
|
266
|
+
return await sample() ? state.partialText(maxChars) : undefined;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
218
269
|
}
|
|
219
270
|
export class DirectAllocationError extends Error {
|
|
220
271
|
allocation;
|
|
@@ -371,28 +422,12 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
371
422
|
const state = inspect(accepted, "agent_prompted", name, paneId, tabId);
|
|
372
423
|
if (state === "blocked" || state === "unknown")
|
|
373
424
|
throw new Error(`Herdr agent became ${state}; inspect it before retrying.`);
|
|
374
|
-
|
|
375
|
-
let lastUsage = {};
|
|
425
|
+
const session = createDirectSessionReader(sessionFile, prompt);
|
|
376
426
|
return {
|
|
377
427
|
name, tabId, paneId, sessionFile, prompt,
|
|
378
|
-
get activity() { return
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (info.size > SESSION_LIMIT) {
|
|
382
|
-
lastUsage = {};
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
if (info.size !== lastUsageSize) {
|
|
386
|
-
lastUsage = directSessionUsage(await readFile(sessionFile, "utf8"), prompt);
|
|
387
|
-
lastUsageSize = info.size;
|
|
388
|
-
}
|
|
389
|
-
return lastUsage.tokens;
|
|
390
|
-
},
|
|
391
|
-
async partialText(maxChars) {
|
|
392
|
-
if ((await stat(sessionFile)).size > SESSION_LIMIT)
|
|
393
|
-
return undefined;
|
|
394
|
-
return directPartialText(await readFile(sessionFile, "utf8"), prompt, maxChars);
|
|
395
|
-
},
|
|
428
|
+
get activity() { return session.activity; },
|
|
429
|
+
usageTokens: session.usageTokens,
|
|
430
|
+
partialText: session.partialText,
|
|
396
431
|
async answer(maxBytes) {
|
|
397
432
|
const readAnswer = async () => {
|
|
398
433
|
const info = await stat(sessionFile);
|