portable-agent-layer 0.84.1 → 0.84.2
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.
|
@@ -263,7 +263,7 @@ Every agent's turns are measured and its replies filed under the session id the
|
|
|
263
263
|
| Cursor | `beforeSubmitPrompt` → `additional_context`, undocumented | `afterAgentResponse` `text` (`AgentResponse.ts`) |
|
|
264
264
|
| Copilot CLI | `userPromptTransformed` → `modifiedTransformedPrompt` (`PromptTransformed.ts`) | `agentStop` transcript |
|
|
265
265
|
|
|
266
|
-
The Copilot CLI drops what its `userPromptSubmitted` hook returns, so that hook only measures the turn and parks the context; `userPromptTransformed` fires next and appends it to the prompt the model receives. Cursor documents no context field on `beforeSubmitPrompt`, so PAL offers `additional_context` there but logs no interaction hint as sent until it is shown to reach the model; its documented context channel is session start. `test/agent-turns.test.ts` holds each agent to this table.
|
|
266
|
+
The Copilot CLI drops what its `userPromptSubmitted` hook returns, so that hook only measures the turn and parks the context; `userPromptTransformed` fires next and appends it to the prompt the model receives. It also writes the reply to the `agentStop` transcript only after that hook returns, so a stop whose transcript still ends on the user's message hands off to a detached `StopDeferred.ts`, which waits for the reply before running the stop handlers. Cursor documents no context field on `beforeSubmitPrompt`, so PAL offers `additional_context` there but logs no interaction hint as sent until it is shown to reach the model; its documented context channel is session start. `test/agent-turns.test.ts` holds each agent to this table.
|
|
267
267
|
|
|
268
268
|
### Design Principles
|
|
269
269
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached child of a stop hook that returned before the agent wrote its reply
|
|
3
|
+
* to the transcript. argv: the deferred payload's path.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
7
|
+
import { finishDeferredStop } from "./lib/stop";
|
|
8
|
+
|
|
9
|
+
if (isPalSpawnedInference()) process.exit(0);
|
|
10
|
+
|
|
11
|
+
await finishDeferredStop(process.argv[2] ?? "");
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { mkdtemp, rename, writeFile } from "node:fs/promises";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { resolve } from "node:path";
|
|
@@ -42,10 +42,89 @@ export interface StopTurnPayload extends HookTurnPayload {
|
|
|
42
42
|
transcriptPath?: string | null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type Transcript = ReturnType<typeof readTranscriptFile>;
|
|
46
|
+
|
|
47
|
+
interface ReplyWait {
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
intervalMs: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const REPLY_WAIT: ReplyWait = { timeoutMs: 15_000, intervalMs: 200 };
|
|
53
|
+
|
|
54
|
+
function transcriptOf(payload: StopTurnPayload | null): Transcript {
|
|
55
|
+
const transcriptPath = payload?.transcript_path ?? payload?.transcriptPath;
|
|
56
|
+
return transcriptPath ? readTranscriptFile(transcriptPath) : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The Copilot CLI writes the reply to its transcript only after agentStop returns. */
|
|
60
|
+
function replyNotWrittenYet(payload: StopTurnPayload | null, messages: Transcript) {
|
|
61
|
+
return messages.at(-1)?.role === "user" && !hookFinalReply(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function transcriptOnceReplied(
|
|
65
|
+
payload: StopTurnPayload,
|
|
66
|
+
wait: ReplyWait
|
|
67
|
+
): Promise<Transcript | null> {
|
|
68
|
+
const deadline = Date.now() + wait.timeoutMs;
|
|
69
|
+
for (;;) {
|
|
70
|
+
const messages = transcriptOf(payload);
|
|
71
|
+
if (!replyNotWrittenYet(payload, messages)) return messages;
|
|
72
|
+
if (Date.now() >= deadline) return null;
|
|
73
|
+
await Bun.sleep(wait.intervalMs);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deferStopTurn(payload: StopTurnPayload): void {
|
|
78
|
+
try {
|
|
79
|
+
const deferId: string = randomUUID();
|
|
80
|
+
const payloadPath = resolve(paths.state(), `stop-deferred.${deferId}.json`);
|
|
81
|
+
writeFileSync(payloadPath, JSON.stringify(payload), "utf-8");
|
|
82
|
+
const scriptPath = resolve(assets.hooks(), "StopDeferred.ts");
|
|
83
|
+
spawnDetachedInference(scriptPath, [payloadPath], "stop-deferred");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logError("deferStopTurn", err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function takeDeferredPayload(payloadPath: string): StopTurnPayload | null {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(readFileSync(payloadPath, "utf-8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(payloadPath, { force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Runs the stop a hook deferred, once the reply it was waiting for is in the transcript. */
|
|
100
|
+
export async function finishDeferredStop(
|
|
101
|
+
payloadPath: string,
|
|
102
|
+
wait: ReplyWait = REPLY_WAIT
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const payload = takeDeferredPayload(payloadPath);
|
|
105
|
+
if (!payload) return;
|
|
106
|
+
const messages = await transcriptOnceReplied(payload, wait);
|
|
107
|
+
if (!messages) {
|
|
108
|
+
logDebug("finishDeferredStop", "The reply never reached the transcript");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await runTurnStop(payload, messages);
|
|
112
|
+
}
|
|
113
|
+
|
|
45
114
|
/** Everything a stop hook does once the agent's payload is read. */
|
|
46
115
|
export async function stopTurn(payload: StopTurnPayload | null): Promise<void> {
|
|
47
|
-
const
|
|
48
|
-
|
|
116
|
+
const messages = transcriptOf(payload);
|
|
117
|
+
if (payload && replyNotWrittenYet(payload, messages)) {
|
|
118
|
+
deferStopTurn(payload);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await runTurnStop(payload, messages);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function runTurnStop(
|
|
125
|
+
payload: StopTurnPayload | null,
|
|
126
|
+
messages: Transcript
|
|
127
|
+
): Promise<void> {
|
|
49
128
|
if (messages.length >= 2) {
|
|
50
129
|
await runStopHandlers(JSON.stringify(messages), {
|
|
51
130
|
lastAssistantMessage: hookFinalReply(payload),
|