@themoltnet/pi-extension 0.31.2 → 0.32.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/dist/index.js +384 -139
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1965,7 +1965,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
|
|
|
1965
1965
|
...options
|
|
1966
1966
|
});
|
|
1967
1967
|
/**
|
|
1968
|
-
*
|
|
1968
|
+
* Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
|
|
1969
1969
|
*/
|
|
1970
1970
|
var batchDeleteTasks = (options) => (options.client ?? client).delete({
|
|
1971
1971
|
security: [
|
|
@@ -14226,6 +14226,86 @@ var PRODUCER_TASK_TYPES = new Set([
|
|
|
14226
14226
|
"render_pack",
|
|
14227
14227
|
"run_eval"
|
|
14228
14228
|
]);
|
|
14229
|
+
function isNonEmptyString(value) {
|
|
14230
|
+
return typeof value === "string" && value.length > 0;
|
|
14231
|
+
}
|
|
14232
|
+
function criterionWeight(criterion, index) {
|
|
14233
|
+
if (typeof criterion.weight === "number") return criterion.weight;
|
|
14234
|
+
if (typeof criterion.max_score === "number") return criterion.max_score / 100;
|
|
14235
|
+
if (typeof criterion.maxScore === "number") return criterion.maxScore / 100;
|
|
14236
|
+
throw new TaskBuildError([{
|
|
14237
|
+
field: `successCriteria/rubric/criteria/${index}/weight`,
|
|
14238
|
+
message: "criterion is missing weight or max_score"
|
|
14239
|
+
}]);
|
|
14240
|
+
}
|
|
14241
|
+
/**
|
|
14242
|
+
* Normalize authoring-time rubric criteria to canonical MoltNet rubric
|
|
14243
|
+
* criteria. Accepts `{id,title,description,weight}` and
|
|
14244
|
+
* `{name,description,max_score}` style inputs, strips authoring-only fields,
|
|
14245
|
+
* and fills a default scoring mode.
|
|
14246
|
+
*/
|
|
14247
|
+
function normalizeRubricCriteria(criteria, options) {
|
|
14248
|
+
const errors = [];
|
|
14249
|
+
const normalized = criteria.map((criterion, index) => {
|
|
14250
|
+
const id = criterion.id ?? criterion.name;
|
|
14251
|
+
const description = criterion.description ?? criterion.title;
|
|
14252
|
+
if (!isNonEmptyString(id)) errors.push({
|
|
14253
|
+
field: `successCriteria/rubric/criteria/${index}/id`,
|
|
14254
|
+
message: "criterion is missing id or name"
|
|
14255
|
+
});
|
|
14256
|
+
if (!isNonEmptyString(description)) errors.push({
|
|
14257
|
+
field: `successCriteria/rubric/criteria/${index}/description`,
|
|
14258
|
+
message: "criterion is missing description or title"
|
|
14259
|
+
});
|
|
14260
|
+
return {
|
|
14261
|
+
id: id ?? "",
|
|
14262
|
+
description: description ?? "",
|
|
14263
|
+
weight: criterionWeight(criterion, index),
|
|
14264
|
+
scoring: criterion.scoring ?? options?.scoring ?? "llm_score"
|
|
14265
|
+
};
|
|
14266
|
+
});
|
|
14267
|
+
if (errors.length > 0) throw new TaskBuildError(errors);
|
|
14268
|
+
return normalized;
|
|
14269
|
+
}
|
|
14270
|
+
/**
|
|
14271
|
+
* Build a canonical `SuccessCriteria` envelope from rubric/checklist-style
|
|
14272
|
+
* criteria. This keeps rubrics readable at the authoring boundary while
|
|
14273
|
+
* preserving the strict task schema on the wire.
|
|
14274
|
+
*/
|
|
14275
|
+
function buildRubricSuccessCriteria(options) {
|
|
14276
|
+
const rubric = {
|
|
14277
|
+
rubricId: options.rubricId,
|
|
14278
|
+
version: options.version ?? "v1",
|
|
14279
|
+
criteria: normalizeRubricCriteria(options.criteria, { scoring: options.scoring }),
|
|
14280
|
+
...options.contentHash ? { contentHash: options.contentHash } : {},
|
|
14281
|
+
...options.preamble ? { preamble: options.preamble } : {},
|
|
14282
|
+
...options.scope ? { scope: options.scope } : {}
|
|
14283
|
+
};
|
|
14284
|
+
const weightError = validateRubricWeights(rubric);
|
|
14285
|
+
if (weightError) throw new TaskBuildError([{
|
|
14286
|
+
field: "successCriteria/rubric/criteria",
|
|
14287
|
+
message: weightError
|
|
14288
|
+
}]);
|
|
14289
|
+
return {
|
|
14290
|
+
version: 1,
|
|
14291
|
+
rubric
|
|
14292
|
+
};
|
|
14293
|
+
}
|
|
14294
|
+
function resolveJudgeEvalAttemptTarget(target) {
|
|
14295
|
+
if ("judgeEvalTarget" in target && typeof target.judgeEvalTarget === "function") return target.judgeEvalTarget();
|
|
14296
|
+
if ("targetTaskId" in target) return {
|
|
14297
|
+
targetTaskId: target.targetTaskId,
|
|
14298
|
+
targetAttemptN: target.targetAttemptN
|
|
14299
|
+
};
|
|
14300
|
+
if ("taskId" in target) return {
|
|
14301
|
+
targetTaskId: target.taskId,
|
|
14302
|
+
targetAttemptN: target.accepted?.attemptN ?? target.attemptN ?? 1
|
|
14303
|
+
};
|
|
14304
|
+
throw new TaskBuildError([{
|
|
14305
|
+
field: "target",
|
|
14306
|
+
message: "judge_eval_attempt target is missing task id"
|
|
14307
|
+
}]);
|
|
14308
|
+
}
|
|
14229
14309
|
/**
|
|
14230
14310
|
* Fluent, network-free builder for a `tasks.create` body. Encodes the
|
|
14231
14311
|
* non-obvious task schema (context arrays, success-criteria gates,
|
|
@@ -14676,6 +14756,20 @@ function buildJudgeEvalAttempt(input) {
|
|
|
14676
14756
|
return buildTask("judge_eval_attempt", input);
|
|
14677
14757
|
}
|
|
14678
14758
|
/**
|
|
14759
|
+
* Build a `judge_eval_attempt` task from an accepted `run_eval` result (or a
|
|
14760
|
+
* small target tuple) plus human-friendly rubric criteria.
|
|
14761
|
+
*
|
|
14762
|
+
* @param target - A `TaskResultReader` or `{targetTaskId,targetAttemptN}` tuple.
|
|
14763
|
+
* @param options - Rubric metadata and eval/checklist-style criteria.
|
|
14764
|
+
* @returns A typed {@link TaskBuilder}.
|
|
14765
|
+
*/
|
|
14766
|
+
function buildJudgeEvalAttemptForRunEval(target, options) {
|
|
14767
|
+
return buildJudgeEvalAttempt({
|
|
14768
|
+
...resolveJudgeEvalAttemptTarget(target),
|
|
14769
|
+
successCriteria: buildRubricSuccessCriteria(options)
|
|
14770
|
+
});
|
|
14771
|
+
}
|
|
14772
|
+
/**
|
|
14679
14773
|
* Build a `pr_review` task. Requires `subject` + `successCriteria`. Note the
|
|
14680
14774
|
* rubric criteria must use `boolean` scoring for this task type.
|
|
14681
14775
|
*
|
|
@@ -14713,7 +14807,9 @@ var TaskResultReader = class {
|
|
|
14713
14807
|
accepted;
|
|
14714
14808
|
/** Token / cost usage for the accepted attempt, if reported. */
|
|
14715
14809
|
usage;
|
|
14810
|
+
/** Task id for the task whose accepted attempt is being read. */
|
|
14716
14811
|
taskId;
|
|
14812
|
+
/** CID of the accepted attempt output. */
|
|
14717
14813
|
outputCid;
|
|
14718
14814
|
constructor(task, attempt) {
|
|
14719
14815
|
const errors = [];
|
|
@@ -14806,6 +14902,19 @@ var TaskResultReader = class {
|
|
|
14806
14902
|
};
|
|
14807
14903
|
}
|
|
14808
14904
|
/**
|
|
14905
|
+
* Return the target tuple required by `judge_eval_attempt`.
|
|
14906
|
+
*
|
|
14907
|
+
* This intentionally uses the accepted attempt number, not merely the
|
|
14908
|
+
* attempt object passed to the reader, so a downstream judge is pinned to
|
|
14909
|
+
* the producer output that the task accepted.
|
|
14910
|
+
*/
|
|
14911
|
+
judgeEvalTarget() {
|
|
14912
|
+
return {
|
|
14913
|
+
targetTaskId: this.taskId,
|
|
14914
|
+
targetAttemptN: this.accepted.attemptN
|
|
14915
|
+
};
|
|
14916
|
+
}
|
|
14917
|
+
/**
|
|
14809
14918
|
* Build a `TaskRef` that anchors a downstream task to this accepted output
|
|
14810
14919
|
* and points at one persistent task artifact by CID.
|
|
14811
14920
|
*
|
|
@@ -14943,6 +15052,7 @@ function createTasksNamespace(context) {
|
|
|
14943
15052
|
buildAssessBrief,
|
|
14944
15053
|
buildJudgePack,
|
|
14945
15054
|
buildJudgeEvalAttempt,
|
|
15055
|
+
buildJudgeEvalAttemptForRunEval,
|
|
14946
15056
|
buildPrReview,
|
|
14947
15057
|
async readResult(taskOrId) {
|
|
14948
15058
|
const task = typeof taskOrId === "string" ? unwrapResult(await getTask({
|
|
@@ -26299,14 +26409,10 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26299
26409
|
});
|
|
26300
26410
|
return makeFailedOutput("session_setup_failed", message);
|
|
26301
26411
|
}
|
|
26302
|
-
|
|
26303
|
-
let llmErrorMessage = null;
|
|
26304
|
-
let assistantText = "";
|
|
26412
|
+
const turnState = createSessionTurnState();
|
|
26305
26413
|
let reporterError = null;
|
|
26306
26414
|
const usage = finalUsage;
|
|
26307
26415
|
let capAbort = null;
|
|
26308
|
-
let toolUseTurnCount = 0;
|
|
26309
|
-
let bashTimeoutCount = 0;
|
|
26310
26416
|
const maxTurns = opts.maxTurns ?? 0;
|
|
26311
26417
|
const maxBashTimeouts = opts.maxBashTimeouts ?? 3;
|
|
26312
26418
|
cancelListener = wireSessionAbort(reporter.cancelSignal, session);
|
|
@@ -26317,7 +26423,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26317
26423
|
const message = err instanceof Error ? err.message : String(err);
|
|
26318
26424
|
reporterError = {
|
|
26319
26425
|
code: "reporter_failed",
|
|
26320
|
-
message
|
|
26426
|
+
message,
|
|
26427
|
+
retryable: true
|
|
26321
26428
|
};
|
|
26322
26429
|
process.stderr.write(`[reporter] ${message}\n`);
|
|
26323
26430
|
}
|
|
@@ -26340,49 +26447,16 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26340
26447
|
message
|
|
26341
26448
|
}));
|
|
26342
26449
|
};
|
|
26343
|
-
session.subscribe((
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
tool_name: event.toolName,
|
|
26354
|
-
is_error: event.isError,
|
|
26355
|
-
result: event.isError ? truncateForWire(event.result) : void 0
|
|
26356
|
-
}));
|
|
26357
|
-
if (shouldEmitToolCallError(event)) track(emitError("tool_call_error", describeToolErrorMessage(event.result), {
|
|
26358
|
-
tool: event.toolName,
|
|
26359
|
-
result: truncateForWire(event.result)
|
|
26360
|
-
}));
|
|
26361
|
-
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
26362
|
-
bashTimeoutCount += 1;
|
|
26363
|
-
if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
26364
|
-
}
|
|
26365
|
-
} else if (event.type === "turn_end") {
|
|
26366
|
-
const msg = event.message;
|
|
26367
|
-
if (msg?.role === "assistant" && msg.usage) {
|
|
26368
|
-
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
26369
|
-
usage.outputTokens += Math.max(0, msg.usage.output ?? 0);
|
|
26370
|
-
const cr = Math.max(0, msg.usage.cacheRead ?? 0);
|
|
26371
|
-
const cw = Math.max(0, msg.usage.cacheWrite ?? 0);
|
|
26372
|
-
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
26373
|
-
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
26374
|
-
}
|
|
26375
|
-
const stopReason = msg?.stopReason ?? "end_turn";
|
|
26376
|
-
track(emit("turn_end", { stop_reason: stopReason }));
|
|
26377
|
-
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
26378
|
-
toolUseTurnCount += 1;
|
|
26379
|
-
if (toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
26380
|
-
}
|
|
26381
|
-
llmAbort = msg?.stopReason === "error";
|
|
26382
|
-
if (msg?.stopReason === "error") llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
26383
|
-
else llmErrorMessage = null;
|
|
26384
|
-
}
|
|
26385
|
-
});
|
|
26450
|
+
session.subscribe(makeSessionEventHandler({
|
|
26451
|
+
state: turnState,
|
|
26452
|
+
usage,
|
|
26453
|
+
maxTurns,
|
|
26454
|
+
maxBashTimeouts,
|
|
26455
|
+
emit,
|
|
26456
|
+
emitError,
|
|
26457
|
+
track,
|
|
26458
|
+
triggerCapAbort
|
|
26459
|
+
}));
|
|
26386
26460
|
let runError = null;
|
|
26387
26461
|
const runPrompt = (promptText) => promptWithProviderErrorRetries({
|
|
26388
26462
|
session: liveSession,
|
|
@@ -26390,8 +26464,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26390
26464
|
cancelSignal: reporter.cancelSignal,
|
|
26391
26465
|
isCapAborted: () => capAbort !== null,
|
|
26392
26466
|
getProviderErrorState: () => ({
|
|
26393
|
-
llmAbort,
|
|
26394
|
-
llmErrorMessage
|
|
26467
|
+
llmAbort: turnState.llmAbort,
|
|
26468
|
+
llmErrorMessage: turnState.llmErrorMessage
|
|
26395
26469
|
}),
|
|
26396
26470
|
maxRetries: opts.maxProviderErrorRetries ?? 2,
|
|
26397
26471
|
baseDelayMs: opts.providerErrorRetryBaseDelayMs ?? 2e3,
|
|
@@ -26420,7 +26494,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26420
26494
|
isStopped: () => submitRepromptStopped({
|
|
26421
26495
|
cancelled: reporter.cancelSignal.aborted,
|
|
26422
26496
|
capAborted: capAbort !== null,
|
|
26423
|
-
llmAbort
|
|
26497
|
+
llmAbort: turnState.llmAbort
|
|
26424
26498
|
}),
|
|
26425
26499
|
onSubmitReprompt: async (event) => {
|
|
26426
26500
|
await emit("info", event);
|
|
@@ -26441,62 +26515,18 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26441
26515
|
let parsedOutput = null;
|
|
26442
26516
|
let parsedOutputCid = null;
|
|
26443
26517
|
let parseError = null;
|
|
26444
|
-
if (!runError && !llmAbort && !cancelled && !capAbort) {
|
|
26445
|
-
const captured =
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
parsedOutput = null;
|
|
26457
|
-
parsedOutputCid = null;
|
|
26458
|
-
parseError = {
|
|
26459
|
-
code: "output_cid_compute_failed",
|
|
26460
|
-
message: `Captured submit-tool output could not be canonicalized: ${message}`
|
|
26461
|
-
};
|
|
26462
|
-
recordTaskOutputParseResult({
|
|
26463
|
-
taskType: task.taskType,
|
|
26464
|
-
model: opts.model,
|
|
26465
|
-
code: "output_cid_compute_failed"
|
|
26466
|
-
});
|
|
26467
|
-
await emit("error", {
|
|
26468
|
-
message: parseError.message,
|
|
26469
|
-
phase: "output_validation"
|
|
26470
|
-
});
|
|
26471
|
-
}
|
|
26472
|
-
else if (submitToolHandle) {
|
|
26473
|
-
const exhausted = submitToolHandle.getExhaustedValidationFailure();
|
|
26474
|
-
parseError = exhausted ?? {
|
|
26475
|
-
code: "submit_output_missing",
|
|
26476
|
-
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26477
|
-
};
|
|
26478
|
-
if (!exhausted) recordTaskOutputParseResult({
|
|
26479
|
-
taskType: task.taskType,
|
|
26480
|
-
model: opts.model,
|
|
26481
|
-
code: "output_missing"
|
|
26482
|
-
});
|
|
26483
|
-
await emit("error", {
|
|
26484
|
-
message: parseError.message,
|
|
26485
|
-
phase: "output_validation"
|
|
26486
|
-
});
|
|
26487
|
-
} else {
|
|
26488
|
-
const parsed = await parseStructuredTaskOutput(assistantText, task.taskType, {
|
|
26489
|
-
model: opts.model,
|
|
26490
|
-
input: task.input
|
|
26491
|
-
});
|
|
26492
|
-
parsedOutput = parsed.output;
|
|
26493
|
-
parsedOutputCid = parsed.outputCid;
|
|
26494
|
-
parseError = parsed.error;
|
|
26495
|
-
if (parseError) await emit("error", {
|
|
26496
|
-
message: parseError.message,
|
|
26497
|
-
phase: "output_validation"
|
|
26498
|
-
});
|
|
26499
|
-
}
|
|
26518
|
+
if (!runError && !turnState.llmAbort && !cancelled && !capAbort) {
|
|
26519
|
+
const captured = await captureAttemptOutput({
|
|
26520
|
+
taskType: task.taskType,
|
|
26521
|
+
model: opts.model,
|
|
26522
|
+
input: task.input,
|
|
26523
|
+
assistantText: turnState.assistantText,
|
|
26524
|
+
submitToolHandle,
|
|
26525
|
+
emit
|
|
26526
|
+
});
|
|
26527
|
+
parsedOutput = captured.output;
|
|
26528
|
+
parsedOutputCid = captured.outputCid;
|
|
26529
|
+
parseError = captured.error;
|
|
26500
26530
|
}
|
|
26501
26531
|
if (cancelled) return {
|
|
26502
26532
|
taskId: task.id,
|
|
@@ -26527,52 +26557,267 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26527
26557
|
retryable: false
|
|
26528
26558
|
}
|
|
26529
26559
|
};
|
|
26530
|
-
|
|
26531
|
-
const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
|
|
26532
|
-
const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
26533
|
-
return {
|
|
26560
|
+
return buildAttemptResult({
|
|
26534
26561
|
taskId: task.id,
|
|
26535
26562
|
attemptN,
|
|
26536
|
-
status,
|
|
26537
26563
|
output: parsedOutput,
|
|
26538
26564
|
outputCid: parsedOutputCid,
|
|
26539
26565
|
usage,
|
|
26540
26566
|
durationMs: Date.now() - startTime,
|
|
26541
|
-
|
|
26542
|
-
|
|
26543
|
-
|
|
26544
|
-
|
|
26545
|
-
|
|
26546
|
-
};
|
|
26567
|
+
runError,
|
|
26568
|
+
parseError,
|
|
26569
|
+
reporterError,
|
|
26570
|
+
llmAbort: turnState.llmAbort,
|
|
26571
|
+
llmErrorMessage: turnState.llmErrorMessage
|
|
26572
|
+
});
|
|
26547
26573
|
} catch (err) {
|
|
26548
26574
|
return makeFailedOutput("executor_unexpected_error", err instanceof Error ? err.message : String(err));
|
|
26549
26575
|
} finally {
|
|
26550
|
-
|
|
26551
|
-
|
|
26552
|
-
|
|
26553
|
-
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26576
|
+
await cleanupAttempt({
|
|
26577
|
+
cancelSignal: reporter.cancelSignal,
|
|
26578
|
+
cancelListener,
|
|
26579
|
+
session,
|
|
26580
|
+
reporterOpen,
|
|
26581
|
+
reporter,
|
|
26582
|
+
finalUsage,
|
|
26583
|
+
managed,
|
|
26584
|
+
workspace,
|
|
26585
|
+
taskId: task.id,
|
|
26586
|
+
attemptN
|
|
26587
|
+
});
|
|
26588
|
+
}
|
|
26589
|
+
}
|
|
26590
|
+
function createSessionTurnState() {
|
|
26591
|
+
return {
|
|
26592
|
+
assistantText: "",
|
|
26593
|
+
llmAbort: false,
|
|
26594
|
+
llmErrorMessage: null,
|
|
26595
|
+
toolUseTurnCount: 0,
|
|
26596
|
+
bashTimeoutCount: 0
|
|
26597
|
+
};
|
|
26598
|
+
}
|
|
26599
|
+
/**
|
|
26600
|
+
* Build the `AgentSession.subscribe` handler for one attempt: bridges pi
|
|
26601
|
+
* events to the reporter, accumulates token usage and assistant text, and
|
|
26602
|
+
* enforces the bash-timeout and tool-use-turn caps. Extracted from
|
|
26603
|
+
* `executePiTask` so this dense, branch-heavy logic is unit-tested against a
|
|
26604
|
+
* scripted event stream instead of only through a booted VM.
|
|
26605
|
+
*
|
|
26606
|
+
* The handler mutates `deps.state` and `deps.usage` in place; the caller reads
|
|
26607
|
+
* them after `session.prompt()` resolves (by which point `state.llmAbort`
|
|
26608
|
+
* holds the terminal turn's outcome — see the "last-turn wins" note below).
|
|
26609
|
+
*
|
|
26610
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26611
|
+
*/
|
|
26612
|
+
function makeSessionEventHandler(deps) {
|
|
26613
|
+
const { state, usage, maxTurns, maxBashTimeouts, emit, emitError, track, triggerCapAbort } = deps;
|
|
26614
|
+
return (event) => {
|
|
26615
|
+
if (event.type === "message_update") {
|
|
26616
|
+
const ae = event.assistantMessageEvent;
|
|
26617
|
+
if (ae.type === "text_delta") {
|
|
26618
|
+
state.assistantText += ae.delta;
|
|
26619
|
+
track(emit("text_delta", { delta: ae.delta }));
|
|
26560
26620
|
}
|
|
26561
|
-
|
|
26562
|
-
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26621
|
+
} else if (event.type === "tool_execution_start") track(emit("tool_call_start", { tool_name: event.toolName }));
|
|
26622
|
+
else if (event.type === "tool_execution_end") {
|
|
26623
|
+
track(emit("tool_call_end", {
|
|
26624
|
+
tool_name: event.toolName,
|
|
26625
|
+
is_error: event.isError,
|
|
26626
|
+
result: event.isError ? truncateForWire(event.result) : void 0
|
|
26627
|
+
}));
|
|
26628
|
+
if (shouldEmitToolCallError(event)) track(emitError("tool_call_error", describeToolErrorMessage(event.result), {
|
|
26629
|
+
tool: event.toolName,
|
|
26630
|
+
result: truncateForWire(event.result)
|
|
26631
|
+
}));
|
|
26632
|
+
if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
|
|
26633
|
+
state.bashTimeoutCount += 1;
|
|
26634
|
+
if (state.bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${state.bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
|
|
26635
|
+
}
|
|
26636
|
+
} else if (event.type === "turn_end") {
|
|
26637
|
+
const msg = event.message;
|
|
26638
|
+
if (msg?.role === "assistant" && msg.usage) {
|
|
26639
|
+
usage.inputTokens += Math.max(0, msg.usage.input ?? 0);
|
|
26640
|
+
usage.outputTokens += Math.max(0, msg.usage.output ?? 0);
|
|
26641
|
+
const cr = Math.max(0, msg.usage.cacheRead ?? 0);
|
|
26642
|
+
const cw = Math.max(0, msg.usage.cacheWrite ?? 0);
|
|
26643
|
+
if (cr) usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cr;
|
|
26644
|
+
if (cw) usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cw;
|
|
26566
26645
|
}
|
|
26646
|
+
const stopReason = msg?.stopReason ?? "end_turn";
|
|
26647
|
+
track(emit("turn_end", { stop_reason: stopReason }));
|
|
26648
|
+
if (maxTurns > 0 && stopReason !== "end_turn" && stopReason !== "aborted" && stopReason !== "error") {
|
|
26649
|
+
state.toolUseTurnCount += 1;
|
|
26650
|
+
if (state.toolUseTurnCount >= maxTurns) triggerCapAbort("max_turns_exceeded", `Aborted after ${state.toolUseTurnCount} tool-use turns (cap ${maxTurns}).`);
|
|
26651
|
+
}
|
|
26652
|
+
state.llmAbort = msg?.stopReason === "error";
|
|
26653
|
+
if (msg?.stopReason === "error") state.llmErrorMessage = typeof msg.errorMessage === "string" && msg.errorMessage.length > 0 ? msg.errorMessage : null;
|
|
26654
|
+
else state.llmErrorMessage = null;
|
|
26567
26655
|
}
|
|
26568
|
-
|
|
26569
|
-
|
|
26570
|
-
|
|
26656
|
+
};
|
|
26657
|
+
}
|
|
26658
|
+
/**
|
|
26659
|
+
* Resolve the attempt's structured output once the session has finished
|
|
26660
|
+
* cleanly (no run error / provider abort / cancel / cap). Three mutually
|
|
26661
|
+
* exclusive paths, in precedence order:
|
|
26662
|
+
*
|
|
26663
|
+
* 1. Submit tool captured a payload → trust it, compute its CID. A
|
|
26664
|
+
* canonicalization failure becomes `output_cid_compute_failed`.
|
|
26665
|
+
* 2. Submit tool registered but nothing captured → the exhausted-validation
|
|
26666
|
+
* failure wins if present, else `submit_output_missing` (recording the
|
|
26667
|
+
* `output_missing` counter so the never-called path is observable).
|
|
26668
|
+
* 3. No submit tool (legacy task type) → parse the trailing assistant text.
|
|
26669
|
+
*
|
|
26670
|
+
* Extracted from `executePiTask` so this precedence — the part a refactor is
|
|
26671
|
+
* most likely to silently reorder — is unit-tested directly. The caller
|
|
26672
|
+
* still owns the guard deciding whether output capture runs at all.
|
|
26673
|
+
*
|
|
26674
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26675
|
+
*/
|
|
26676
|
+
async function captureAttemptOutput(deps) {
|
|
26677
|
+
const { taskType, model, input, assistantText, submitToolHandle, emit } = deps;
|
|
26678
|
+
const captured = submitToolHandle?.getCaptured() ?? null;
|
|
26679
|
+
if (captured) try {
|
|
26680
|
+
const outputCid = await computeJsonCid(captured);
|
|
26681
|
+
recordTaskOutputParseResult({
|
|
26682
|
+
taskType,
|
|
26683
|
+
model,
|
|
26684
|
+
code: "captured_via_tool"
|
|
26685
|
+
});
|
|
26686
|
+
return {
|
|
26687
|
+
output: captured,
|
|
26688
|
+
outputCid,
|
|
26689
|
+
error: null
|
|
26690
|
+
};
|
|
26691
|
+
} catch (err) {
|
|
26692
|
+
const error = {
|
|
26693
|
+
code: "output_cid_compute_failed",
|
|
26694
|
+
message: `Captured submit-tool output could not be canonicalized: ${err instanceof Error ? err.message : String(err)}`
|
|
26695
|
+
};
|
|
26696
|
+
recordTaskOutputParseResult({
|
|
26697
|
+
taskType,
|
|
26698
|
+
model,
|
|
26699
|
+
code: "output_cid_compute_failed"
|
|
26700
|
+
});
|
|
26701
|
+
await emit("error", {
|
|
26702
|
+
message: error.message,
|
|
26703
|
+
phase: "output_validation"
|
|
26704
|
+
});
|
|
26705
|
+
return {
|
|
26706
|
+
output: null,
|
|
26707
|
+
outputCid: null,
|
|
26708
|
+
error
|
|
26709
|
+
};
|
|
26710
|
+
}
|
|
26711
|
+
if (submitToolHandle) {
|
|
26712
|
+
const exhausted = submitToolHandle.getExhaustedValidationFailure();
|
|
26713
|
+
const error = exhausted ?? {
|
|
26714
|
+
code: "submit_output_missing",
|
|
26715
|
+
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26716
|
+
};
|
|
26717
|
+
if (!exhausted) recordTaskOutputParseResult({
|
|
26718
|
+
taskType,
|
|
26719
|
+
model,
|
|
26720
|
+
code: "output_missing"
|
|
26721
|
+
});
|
|
26722
|
+
await emit("error", {
|
|
26723
|
+
message: error.message,
|
|
26724
|
+
phase: "output_validation"
|
|
26725
|
+
});
|
|
26726
|
+
return {
|
|
26727
|
+
output: null,
|
|
26728
|
+
outputCid: null,
|
|
26729
|
+
error
|
|
26730
|
+
};
|
|
26731
|
+
}
|
|
26732
|
+
const parsed = await parseStructuredTaskOutput(assistantText, taskType, {
|
|
26733
|
+
model,
|
|
26734
|
+
input
|
|
26735
|
+
});
|
|
26736
|
+
if (parsed.error) await emit("error", {
|
|
26737
|
+
message: parsed.error.message,
|
|
26738
|
+
phase: "output_validation"
|
|
26739
|
+
});
|
|
26740
|
+
return {
|
|
26741
|
+
output: parsed.output,
|
|
26742
|
+
outputCid: parsed.outputCid,
|
|
26743
|
+
error: parsed.error
|
|
26744
|
+
};
|
|
26745
|
+
}
|
|
26746
|
+
/**
|
|
26747
|
+
* Assemble the terminal `TaskOutput` for a clean-or-failed finish (cancel and
|
|
26748
|
+
* cap aborts are handled by the caller's earlier returns). Encapsulates the
|
|
26749
|
+
* failure-precedence ladder — runError → parseError → reporterError →
|
|
26750
|
+
* provider abort — so the ordering is unit-tested rather than buried in the
|
|
26751
|
+
* orchestrator. A provider abort with no captured diagnostic falls back to a
|
|
26752
|
+
* generic message.
|
|
26753
|
+
*
|
|
26754
|
+
* Errors are non-retryable EXCEPT a reporterError that both wins the ladder
|
|
26755
|
+
* and set `retryable: true` (a transient reporter failure, #1538).
|
|
26756
|
+
*
|
|
26757
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26758
|
+
*/
|
|
26759
|
+
function buildAttemptResult(args) {
|
|
26760
|
+
const status = args.runError || args.llmAbort || args.parseError || args.reporterError ? "failed" : "completed";
|
|
26761
|
+
const errorCode = args.runError?.code ?? args.parseError?.code ?? args.reporterError?.code ?? (args.llmAbort ? "llm_api_error" : void 0);
|
|
26762
|
+
const errorMessage = args.runError?.message ?? args.parseError?.message ?? args.reporterError?.message ?? (args.llmAbort ? args.llmErrorMessage ?? "LLM API error during turn" : void 0);
|
|
26763
|
+
const errorRetryable = args.reporterError && errorCode === args.reporterError.code && errorMessage === args.reporterError.message ? args.reporterError.retryable ?? false : false;
|
|
26764
|
+
return {
|
|
26765
|
+
taskId: args.taskId,
|
|
26766
|
+
attemptN: args.attemptN,
|
|
26767
|
+
status,
|
|
26768
|
+
output: args.output,
|
|
26769
|
+
outputCid: args.outputCid,
|
|
26770
|
+
usage: args.usage,
|
|
26771
|
+
durationMs: args.durationMs,
|
|
26772
|
+
...errorCode && errorMessage ? { error: {
|
|
26773
|
+
code: errorCode,
|
|
26774
|
+
message: errorMessage,
|
|
26775
|
+
retryable: errorRetryable
|
|
26776
|
+
} } : {}
|
|
26777
|
+
};
|
|
26778
|
+
}
|
|
26779
|
+
/**
|
|
26780
|
+
* Tear down one attempt's resources, in order: detach the cancel listener →
|
|
26781
|
+
* dispose the pi session → finalize+close the reporter → close the VM →
|
|
26782
|
+
* clean the workspace. Extracted from `executePiTask`'s `finally` so the
|
|
26783
|
+
* swallow-vs-log-vs-propagate policy is pinned by tests.
|
|
26784
|
+
*
|
|
26785
|
+
* Failure handling is deliberately asymmetric and preserved exactly:
|
|
26786
|
+
* `session.dispose()` throws are silently swallowed; reporter finalize/close
|
|
26787
|
+
* and workspace cleanup failures are logged but non-fatal (the task is about
|
|
26788
|
+
* to be reported anyway). `vm.close()` is the one teardown error allowed to
|
|
26789
|
+
* propagate: a leaked VM means a live microVM the host never reclaims, so its
|
|
26790
|
+
* failure must surface loudly rather than be logged and forgotten.
|
|
26791
|
+
*
|
|
26792
|
+
* @internal Exported for unit testing; not part of the package's public API.
|
|
26793
|
+
*/
|
|
26794
|
+
async function cleanupAttempt(deps) {
|
|
26795
|
+
const log = deps.logError ?? ((m) => console.error(m));
|
|
26796
|
+
if (deps.cancelListener) deps.cancelSignal.removeEventListener("abort", deps.cancelListener);
|
|
26797
|
+
if (deps.session) try {
|
|
26798
|
+
deps.session.dispose();
|
|
26799
|
+
} catch {}
|
|
26800
|
+
if (deps.reporterOpen) {
|
|
26801
|
+
try {
|
|
26802
|
+
await deps.reporter.finalize(deps.finalUsage);
|
|
26803
|
+
} catch (err) {
|
|
26804
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
26805
|
+
log(`executePiTask: reporter.finalize() failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26806
|
+
}
|
|
26807
|
+
try {
|
|
26808
|
+
await deps.reporter.close();
|
|
26571
26809
|
} catch (err) {
|
|
26572
26810
|
const detail = err instanceof Error ? err.message : String(err);
|
|
26573
|
-
|
|
26811
|
+
log(`executePiTask: reporter.close() failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26574
26812
|
}
|
|
26575
26813
|
}
|
|
26814
|
+
if (deps.managed) await deps.managed.vm.close();
|
|
26815
|
+
if (deps.workspace) try {
|
|
26816
|
+
deps.workspace.cleanup();
|
|
26817
|
+
} catch (err) {
|
|
26818
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
26819
|
+
log(`executePiTask: workspace cleanup failed for task ${deps.taskId} attempt ${deps.attemptN}: ${detail}`);
|
|
26820
|
+
}
|
|
26576
26821
|
}
|
|
26577
26822
|
function applyExecutionPlanSandboxOverrides(sandboxConfig, executionPlan) {
|
|
26578
26823
|
const shadowWrites = executionPlan?.workspaceAttachment?.shadowWrites;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/pi-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
|
|
6
6
|
"keywords": [
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@earendil-works/gondolin": "^0.9.1",
|
|
37
37
|
"@opentelemetry/api": "^1.9.0",
|
|
38
38
|
"typebox": "^1.2.8",
|
|
39
|
-
"@themoltnet/sdk": "0.
|
|
40
|
-
"@themoltnet/agent-runtime": "0.
|
|
39
|
+
"@themoltnet/sdk": "0.119.0",
|
|
40
|
+
"@themoltnet/agent-runtime": "0.34.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|