mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
Clarify prepareTurn request projection semantics
This commit is contained in:
@@ -206,6 +206,40 @@ new Agent({
|
||||
For richer, host-side hook orchestration (15-stage `HookEngine`,
|
||||
subprocess-backed hooks, MCP extensions), use `@cline/core`.
|
||||
|
||||
### Request Projection with `prepareTurn`
|
||||
|
||||
`prepareTurn` is a request projection hook. It runs during turn preparation and
|
||||
may return a different message list or system prompt for the next provider
|
||||
request:
|
||||
|
||||
```text
|
||||
canonical transcript
|
||||
|
|
||||
| turn preparation
|
||||
v
|
||||
prepareTurn
|
||||
|
|
||||
v
|
||||
provider request
|
||||
```
|
||||
|
||||
Returned messages affect only the provider request for the current model call.
|
||||
They do not replace the runtime's canonical transcript, are not persisted as
|
||||
session history, and are not returned from `AgentRunResult.messages`.
|
||||
|
||||
```text
|
||||
prepareTurn returns projected messages
|
||||
|
|
||||
+--> provider request: yes
|
||||
+--> canonical transcript: no
|
||||
+--> persisted transcript: no
|
||||
+--> AgentRunResult.messages: no
|
||||
```
|
||||
|
||||
This is intentionally different from a transcript rewrite. Hosts that need
|
||||
durable redaction, normalization, or policy filtering must apply that change
|
||||
before a message enters the agent's canonical transcript.
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins can contribute tools and hooks at setup time:
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("AgentRuntime", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("injects pending user messages before prepareTurn rewrites the transcript", async () => {
|
||||
it("injects pending user messages before prepareTurn projects the provider request", async () => {
|
||||
const consumePendingUserMessage = vi.fn(() => "steer before prepare");
|
||||
const prepareTurn = vi.fn(
|
||||
(context: { messages: readonly AgentMessage[] }) => ({
|
||||
@@ -261,7 +261,7 @@ describe("AgentRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets prepareTurn compact tool results after pending user input is added", async () => {
|
||||
it("lets prepareTurn project tool results after pending user input is added", async () => {
|
||||
const consumePendingUserMessage = vi.fn(() => "latest steering");
|
||||
const hugeToolOutput = "x".repeat(100_000);
|
||||
const prepareTurn = vi.fn(
|
||||
@@ -1180,11 +1180,11 @@ describe("AgentRuntime", () => {
|
||||
expect(model.requests).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("runs prepareTurn before beforeModel without overwriting canonical messages", async () => {
|
||||
const compactedMessage: AgentMessage = {
|
||||
id: "msg_compacted",
|
||||
it("projects the provider request without overwriting canonical messages", async () => {
|
||||
const projectedMessage: AgentMessage = {
|
||||
id: "msg_projected",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "compacted context" }],
|
||||
content: [{ type: "text", text: "projected context" }],
|
||||
createdAt: 1,
|
||||
};
|
||||
const notices: string[] = [];
|
||||
@@ -1197,19 +1197,19 @@ describe("AgentRuntime", () => {
|
||||
reason: "auto_compaction",
|
||||
});
|
||||
return {
|
||||
messages: [compactedMessage],
|
||||
systemPrompt: "compacted system",
|
||||
messages: [projectedMessage],
|
||||
systemPrompt: "projected system",
|
||||
};
|
||||
});
|
||||
const beforeModel = vi.fn(({ request }) => {
|
||||
expect(request.systemPrompt).toBe("compacted system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.systemPrompt).toBe("projected system");
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return undefined;
|
||||
});
|
||||
const model = new ScriptedModel([
|
||||
(request) => {
|
||||
expect(request.systemPrompt).toBe("compacted system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.systemPrompt).toBe("projected system");
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return [
|
||||
{ type: "text-delta", text: "done" },
|
||||
{ type: "finish", reason: "stop" },
|
||||
@@ -1233,11 +1233,13 @@ describe("AgentRuntime", () => {
|
||||
expect(prepareTurn).toHaveBeenCalledTimes(1);
|
||||
expect(beforeModel).toHaveBeenCalledTimes(1);
|
||||
expect(notices).toEqual(["auto-compacting"]);
|
||||
expect(model.requests[0]?.messages).toEqual([projectedMessage]);
|
||||
expect(result.messages[0]).toMatchObject({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "large context" }],
|
||||
});
|
||||
expect(result.messages).toHaveLength(2);
|
||||
expect(result.messages).not.toContainEqual(projectedMessage);
|
||||
expect(model.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -1343,16 +1345,16 @@ describe("AgentRuntime", () => {
|
||||
});
|
||||
|
||||
it("preserves the existing system prompt when prepareTurn returns only messages", async () => {
|
||||
const compactedMessage: AgentMessage = {
|
||||
id: "msg_compacted",
|
||||
const projectedMessage: AgentMessage = {
|
||||
id: "msg_projected",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "compacted context" }],
|
||||
content: [{ type: "text", text: "projected context" }],
|
||||
createdAt: 1,
|
||||
};
|
||||
const model = new ScriptedModel([
|
||||
(request) => {
|
||||
expect(request.systemPrompt).toBe("original system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return [
|
||||
{ type: "text-delta", text: "done" },
|
||||
{ type: "finish", reason: "stop" },
|
||||
@@ -1362,7 +1364,7 @@ describe("AgentRuntime", () => {
|
||||
const runtime = new AgentRuntime({
|
||||
model,
|
||||
systemPrompt: "original system",
|
||||
prepareTurn: () => ({ messages: [compactedMessage] }),
|
||||
prepareTurn: () => ({ messages: [projectedMessage] }),
|
||||
});
|
||||
|
||||
await runtime.run("large context");
|
||||
|
||||
@@ -438,9 +438,11 @@ export interface AgentRuntimeConfig {
|
||||
request: ToolApprovalRequest,
|
||||
) => Promise<ToolApprovalResult> | ToolApprovalResult;
|
||||
/**
|
||||
* Optional host-owned context pipeline that can project the transcript before
|
||||
* each model request. Returned messages affect the provider request only; the
|
||||
* runtime's canonical in-memory transcript remains append-only.
|
||||
* Optional host-owned request projection hook invoked before each model call.
|
||||
*
|
||||
* Returned messages affect only the provider request for the current call.
|
||||
* They do not replace the canonical runtime transcript, are not persisted as
|
||||
* session history, and are not reflected in AgentRunResult.messages.
|
||||
*/
|
||||
prepareTurn?: (
|
||||
context: AgentRuntimePrepareTurnContext,
|
||||
|
||||
@@ -800,9 +800,14 @@ export interface AgentConfig {
|
||||
*/
|
||||
logger?: BasicLogger;
|
||||
/**
|
||||
* Optional callback that can project the turn input before each model call.
|
||||
* Returned messages affect the provider request only; the canonical runtime
|
||||
* transcript remains append-only.
|
||||
* Optional request projection hook invoked before each model call.
|
||||
*
|
||||
* Returned messages affect only the provider request for the current call.
|
||||
* They do not replace the canonical runtime transcript, are not persisted as
|
||||
* session history, and are not reflected in AgentRunResult.messages.
|
||||
*
|
||||
* Hosts that need durable redaction or normalization must apply it before a
|
||||
* message enters the canonical transcript.
|
||||
*/
|
||||
prepareTurn?: (
|
||||
context: AgentPrepareTurnContext,
|
||||
|
||||
Reference in New Issue
Block a user