Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion pi/pi-extensions/ainsel-runner/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { extractEventContext, createTurnTracker, beginTurn, endTurn, recordAssistantEnd, captureMessage, markSettled, waitForSettle, toConversationPayloads, postTaskMessages, reportConversation, redactSecrets, resetRedactionCache, capToolResultContent, truncateWithMarker, resolveToolResultMaxChars, resolveInternalToken } from "./index.ts";
import { extractEventContext, buildUserMessage, createTurnTracker, beginTurn, endTurn, recordAssistantEnd, captureMessage, markSettled, waitForSettle, toConversationPayloads, postTaskMessages, reportConversation, redactSecrets, resetRedactionCache, capToolResultContent, truncateWithMarker, resolveToolResultMaxChars, resolveEventDataMaxChars, resolveInternalToken } from "./index.ts";
import type { HubEvent, TurnTracker, ConversationPayload } from "./index.ts";

describe("extractEventContext", () => {
Expand Down Expand Up @@ -194,6 +194,105 @@ describe("extractEventContext", () => {
});
});

describe("buildUserMessage", () => {
const webhookCtx = {
type: "issue_comment.created",
actor: "alice",
owner: "acme",
repo: "web",
number: 83,
kind: "issue",
id: "evt-1",
};

it("inlines the raw payload in an event-data block", () => {
const payload = {
action: "created",
comment: { body: "@review-agent wdyt?" },
};
const msg = buildUserMessage(webhookCtx, payload);

assert.ok(msg.includes("type: issue_comment.created"));
assert.ok(msg.includes("issue: 83"));
assert.ok(msg.includes("<event-data>"));
assert.ok(msg.includes("</event-data>"));
assert.ok(msg.includes("@review-agent wdyt?"));
assert.ok(msg.includes('"action": "created"'));
});

it("omits the event-data block when no payload is given", () => {
const msg = buildUserMessage(webhookCtx);
assert.ok(!msg.includes("<event-data>"));
assert.ok(msg.includes("Handle the issue_comment.created"));
});

it("omits the event-data block for null payloads", () => {
const msg = buildUserMessage(webhookCtx, null);
assert.ok(!msg.includes("<event-data>"));
});

it("truncates oversized payloads with a marker", () => {
const prev = process.env.EVENT_DATA_MAX_CHARS;
process.env.EVENT_DATA_MAX_CHARS = "200";
try {
const payload = { blob: "x".repeat(5000) };
const msg = buildUserMessage(webhookCtx, payload);
const start = msg.indexOf("<event-data>") + "<event-data>\n".length;
const end = msg.lastIndexOf("</event-data>");
const body = msg.slice(start, end);
assert.ok(body.includes("[truncated"), "payload must carry a truncation marker");
assert.ok(body.length < 500, "payload must be capped near the limit");
} finally {
if (prev === undefined) delete process.env.EVENT_DATA_MAX_CHARS;
else process.env.EVENT_DATA_MAX_CHARS = prev;
}
});

it("does not inline payload for chat.message events", () => {
const msg = buildUserMessage({ type: "chat.message", chatSessionId: "s1", chatMessage: "hi" });
assert.ok(!msg.includes("<event-data>"));
assert.ok(msg.includes("hi"));
});
});

describe("resolveEventDataMaxChars", () => {
it("returns the default when unset", () => {
const prev = process.env.EVENT_DATA_MAX_CHARS;
delete process.env.EVENT_DATA_MAX_CHARS;
try {
assert.equal(resolveEventDataMaxChars(), 16_000);
} finally {
if (prev !== undefined) process.env.EVENT_DATA_MAX_CHARS = prev;
}
});

it("ignores invalid values", () => {
const prev = process.env.EVENT_DATA_MAX_CHARS;
try {
process.env.EVENT_DATA_MAX_CHARS = "banana";
assert.equal(resolveEventDataMaxChars(), 16_000);
process.env.EVENT_DATA_MAX_CHARS = "-5";
assert.equal(resolveEventDataMaxChars(), 16_000);
process.env.EVENT_DATA_MAX_CHARS = "0";
assert.equal(resolveEventDataMaxChars(), 16_000);
} finally {
if (prev === undefined) delete process.env.EVENT_DATA_MAX_CHARS;
else process.env.EVENT_DATA_MAX_CHARS = prev;
}
});

it("honours a valid override", () => {
const prev = process.env.EVENT_DATA_MAX_CHARS;
try {
process.env.EVENT_DATA_MAX_CHARS = "512";
assert.equal(resolveEventDataMaxChars(), 512);
} finally {
if (prev === undefined) delete process.env.EVENT_DATA_MAX_CHARS;
else process.env.EVENT_DATA_MAX_CHARS = prev;
}
});
});

describe("toConversationPayloads", () => {
const meta = {
agentName: "olli",
Expand Down
30 changes: 28 additions & 2 deletions pi/pi-extensions/ainsel-runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
* NAK_DELAY_MS default 60000 — NACK backoff (ms) before retry
* TOOL_RESULT_MAX_CHARS default 16000 — per-text-block cap for toolResult
* transcript content (chars, UTF-16 code units)
* EVENT_DATA_MAX_CHARS default 16000 — cap for the raw event payload
* inlined into the prompt as <event-data>
*/

import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
Expand Down Expand Up @@ -51,6 +53,7 @@ const REDACTION_DENYLIST = new Set([
"PWD",
"NAK_DELAY_MS",
"TOOL_RESULT_MAX_CHARS",
"EVENT_DATA_MAX_CHARS",
"POST_TIMEOUT_MS",
"TURN_TIMEOUT_MS",
"TURN_SETTLE_TIMEOUT_MS",
Expand Down Expand Up @@ -122,6 +125,21 @@ export function resetRedactionCache(): void {
/** Default per-text-block cap for toolResult content (chars). */
const TOOL_RESULT_MAX_CHARS_DEFAULT = 16_000;

const EVENT_DATA_MAX_CHARS_DEFAULT = 16_000;

/**
* Resolve the raw-event-payload cap from the `EVENT_DATA_MAX_CHARS` env var,
* falling back to EVENT_DATA_MAX_CHARS_DEFAULT for unset/invalid values.
*/
export function resolveEventDataMaxChars(): number {
const env = process.env.EVENT_DATA_MAX_CHARS;
if (env) {
const n = parseInt(env, 10);
if (!Number.isNaN(n) && n > 0) return n;
}
return EVENT_DATA_MAX_CHARS_DEFAULT;
}

/**
* Resolve the per-text-block cap from the `TOOL_RESULT_MAX_CHARS` env var,
* read lazily at call time so tests can override it without module reload.
Expand Down Expand Up @@ -525,7 +543,7 @@ function startMetricsServer() {
});
}

function buildUserMessage(ctx: EventContext): string {
export function buildUserMessage(ctx: EventContext, payload?: unknown): string {
if (ctx.type === "chat.message") {
const lines = [
"<event>",
Expand Down Expand Up @@ -580,6 +598,14 @@ function buildUserMessage(ctx: EventContext): string {
lines.push("</event>");
lines.push("");
lines.push(`Handle the ${ctx.type ?? "event"} on ${ref}.`);
if (payload !== undefined && payload !== null) {
lines.push("");
lines.push("The full raw event payload:");
lines.push("");
lines.push("<event-data>");
lines.push(truncateWithMarker(JSON.stringify(payload, null, 2), resolveEventDataMaxChars()));
lines.push("</event-data>");
}
return lines.join("\n");
}

Expand Down Expand Up @@ -924,7 +950,7 @@ async function processTask(
const thisGeneration = tracker.activeGeneration;

try {
await pi.sendUserMessage(buildUserMessage(evCtx));
await pi.sendUserMessage(buildUserMessage(evCtx, task.payload));

// Wait for the turn to end, with a timeout to prevent hanging forever.
let turnTimer: ReturnType<typeof setTimeout> | undefined;
Expand Down