Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const emitOverlappingXAiPromptCompleteOutOfOrder =
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
const emitContextUsage = process.env.T3_ACP_EMIT_CONTEXT_USAGE === "1";
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
const initialGrokReasoningEffort =
process.env.T3_ACP_INITIAL_GROK_REASONING_EFFORT?.trim() || undefined;
Expand Down Expand Up @@ -1061,6 +1062,7 @@ const program = Effect.gen(function* () {

yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
...(emitContextUsage ? { _meta: { totalTokens: 1615 } } : {}),
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: promptResponseText ?? "hello from mock" },
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
makeAcpPlanUpdatedEvent,
makeAcpRequestOpenedEvent,
makeAcpRequestResolvedEvent,
makeAcpTokenUsageEvent,
makeAcpToolCallEvent,
} from "../acp/AcpCoreRuntimeEvents.ts";
import {
Expand Down Expand Up @@ -867,6 +868,27 @@ export function makeCursorAdapter(
}),
);
return;
case "UsageUpdated": {
yield* logNative(
ctx.threadId,
"session/update",
event.rawPayload,
"acp.jsonrpc",
);
const usageEvent = makeAcpTokenUsageEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: ctx.activeTurnId,
used: event.used,
size: event.size,
rawPayload: event.rawPayload,
});
if (usageEvent) {
yield* offerRuntimeEvent(usageEvent);
}
return;
}
}
}),
),
Expand Down
50 changes: 50 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,56 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

it.effect("emits context occupancy from session-update _meta.totalTokens", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-context-window-thread");
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({ T3_ACP_EMIT_CONTEXT_USAGE: "1" }),
);
const adapter = yield* makeTestAdapter(wrapperPath);

const runtimeEvents: ProviderRuntimeEvent[] = [];
const turnCompleted = yield* Deferred.make<void>();
const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) =>
Effect.sync(() => {
runtimeEvents.push(event);
}).pipe(
Effect.andThen(
event.type === "turn.completed"
? Deferred.succeed(turnCompleted, undefined)
: Effect.void,
),
),
).pipe(Effect.forkChild);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-4.6" },
});

yield* adapter.sendTurn({
threadId,
input: "hello grok",
attachments: [],
});

yield* Deferred.await(turnCompleted);
yield* Fiber.interrupt(runtimeEventsFiber);

const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated");
assert.equal(usageEvent?.type, "thread.token-usage.updated");
if (usageEvent?.type === "thread.token-usage.updated") {
assert.equal(usageEvent.payload.usage.usedTokens, 1615);
assert.equal(usageEvent.payload.usage.maxTokens, 500_000);
}

yield* adapter.stopSession(threadId);
}),
);

it.effect("closes the ACP child process when a session stops", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-stop-session-close");
Expand Down
44 changes: 43 additions & 1 deletion apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ import {
makeAcpPlanUpdatedEvent,
makeAcpRequestOpenedEvent,
makeAcpRequestResolvedEvent,
makeAcpTokenUsageEvent,
makeAcpToolCallEvent,
} from "../acp/AcpCoreRuntimeEvents.ts";
import { parsePermissionRequest } from "../acp/AcpRuntimeModel.ts";
import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts";
import {
applyGrokAcpModelSelection,
contextWindowFromGrokSessionSetup,
currentGrokModelIdFromSessionSetup,
currentGrokReasoningEffortFromSessionSetup,
makeGrokAcpRuntime,
Expand Down Expand Up @@ -169,6 +171,8 @@ interface GrokSessionContext {
promptResponsesReady: number;
currentModelId: string | undefined;
currentReasoningEffort: string | undefined;
lastContextTokens: number | undefined;
lastKnownMaxTokens: number | undefined;
stopped: boolean;
}

Expand Down Expand Up @@ -314,6 +318,8 @@ function selectAutoApprovedPermissionOption(
);
}

const GROK_DEFAULT_CONTEXT_WINDOW = 500_000;

function completedStopReasonFromPromptResponse(
response: EffectAcpSchema.PromptResponse | undefined,
): EffectAcpSchema.StopReason | null {
Expand Down Expand Up @@ -1300,6 +1306,11 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
requestedStartReasoningEffort !== undefined
? normalizeGrokReasoningEffort(requestedStartReasoningEffort)
: currentStartReasoningEffort,
lastContextTokens: undefined,
lastKnownMaxTokens: contextWindowFromGrokSessionSetup(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
started.sessionSetupResult,
boundModelId,
),
stopped: false,
};

Expand All @@ -1313,7 +1324,8 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
if (
event._tag === "PlanUpdated" ||
event._tag === "ToolCallUpdated" ||
event._tag === "ContentDelta"
event._tag === "ContentDelta" ||
event._tag === "UsageUpdated"
) {
yield* logNative(ctx.threadId, "session/update", event.rawPayload);
}
Expand All @@ -1322,6 +1334,36 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
return;
}

if (event._tag === "UsageUpdated") {
const usageTurnId = resolveNotificationTurnId(ctx);
if (usageTurnId !== undefined && ctx.interruptedTurnIds.has(usageTurnId)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/GrokAdapter.ts:1374

A queued UsageUpdated from a cancelled prompt is emitted as thread.token-usage.updated after interruptTurn clears activeTurnId, so stale usage overwrites the composer context meter. Treat usageTurnId === undefined as stale here, in addition to checking interruptedTurnIds.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/GrokAdapter.ts around line 1374:

A queued `UsageUpdated` from a cancelled prompt is emitted as `thread.token-usage.updated` after `interruptTurn` clears `activeTurnId`, so stale usage overwrites the composer context meter. Treat `usageTurnId === undefined` as stale here, in addition to checking `interruptedTurnIds`.

return;
}
if (ctx.lastContextTokens === event.used) {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
return;
}
ctx.lastContextTokens = event.used;
if (event.size > 0) {
ctx.lastKnownMaxTokens = event.size;
}
const usageEvent = makeAcpTokenUsageEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: usageTurnId,
used: event.used,
size:
event.size > 0
? event.size
: (ctx.lastKnownMaxTokens ?? GROK_DEFAULT_CONTEXT_WINDOW),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
rawPayload: event.rawPayload,
});
if (usageEvent) {
yield* offerRuntimeEvent(usageEvent);
}
return;
}

const notificationTurnId = resolveNotificationTurnId(ctx);
if (
notificationTurnId === undefined ||
Expand Down
33 changes: 33 additions & 0 deletions apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
makeAcpPlanUpdatedEvent,
makeAcpRequestOpenedEvent,
makeAcpRequestResolvedEvent,
makeAcpTokenUsageEvent,
makeAcpToolCallEvent,
} from "./AcpCoreRuntimeEvents.ts";

Expand Down Expand Up @@ -191,5 +192,37 @@ describe("AcpCoreRuntimeEvents", () => {
status: "inProgress",
},
});

expect(
makeAcpTokenUsageEvent({
stamp,
provider: ProviderDriverKind.make("grok"),
threadId: "thread-1" as never,
turnId,
used: 12_345,
size: 200_000,
rawPayload: { sessionUpdate: "usage_update" },
}),
).toMatchObject({
type: "thread.token-usage.updated",
payload: {
usage: {
usedTokens: 12_345,
maxTokens: 200_000,
},
},
});

expect(
makeAcpTokenUsageEvent({
stamp,
provider: ProviderDriverKind.make("grok"),
threadId: "thread-1" as never,
turnId,
used: 0,
size: 200_000,
rawPayload: { sessionUpdate: "usage_update" },
}),
).toBeNull();
});
});
39 changes: 39 additions & 0 deletions apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type ProviderRuntimeEvent,
type RuntimeRequestId,
type ThreadId,
type ThreadTokenUsageSnapshot,
type TurnId,
} from "@t3tools/contracts";

Expand Down Expand Up @@ -228,3 +229,41 @@ export function makeAcpContentDeltaEvent(input: {
},
};
}

export function makeAcpTokenUsageEvent(input: {
readonly stamp: AcpEventStamp;
readonly provider: ProviderDriverKind;
readonly threadId: ThreadId;
readonly turnId: TurnId | undefined;
readonly used: number;
readonly size: number;
readonly rawPayload: unknown;
}): ProviderRuntimeEvent | null {
if (!Number.isFinite(input.used) || input.used <= 0) {
return null;
}
const usedTokens = Math.round(input.used);
const maxTokens =
Number.isFinite(input.size) && input.size >= 1 ? Math.round(input.size) : undefined;
const clamped = maxTokens !== undefined ? Math.min(usedTokens, maxTokens) : usedTokens;
const usage: ThreadTokenUsageSnapshot = {
usedTokens: clamped,
lastUsedTokens: clamped,
...(maxTokens !== undefined ? { maxTokens } : {}),
};
return {
type: "thread.token-usage.updated",
...input.stamp,
provider: input.provider,
threadId: input.threadId,
turnId: input.turnId,
payload: {
usage,
},
raw: {
source: "acp.jsonrpc",
method: "session/update",
payload: input.rawPayload,
},
};
}
74 changes: 74 additions & 0 deletions apps/server/src/provider/acp/AcpRuntimeModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,80 @@ describe("AcpRuntimeModel", () => {
]);
});

it("reads Grok context occupancy from session-update _meta.totalTokens", () => {
const result = parseSessionUpdateEvent({
sessionId: "session-1",
_meta: { totalTokens: 1615, contextWindowTokens: 500_000 },
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "hello",
},
},
} satisfies EffectAcpSchema.SessionNotification);

expect(result.events).toContainEqual({
_tag: "UsageUpdated",
used: 1615,
size: 500_000,
rawPayload: {
sessionId: "session-1",
_meta: { totalTokens: 1615, contextWindowTokens: 500_000 },
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "hello",
},
},
},
});
});

it("does not treat billed prompt-response usage as context occupancy", () => {
const result = parseSessionUpdateEvent({
sessionId: "session-1",
_meta: { usage: { totalTokens: 20_554, inputTokens: 18_000, outputTokens: 2_554 } },
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "hello",
},
},
} satisfies EffectAcpSchema.SessionNotification);

expect(result.events.some((event) => event._tag === "UsageUpdated")).toBe(false);
});

it("projects ACP usage updates for the context-window meter", () => {
const result = parseSessionUpdateEvent({
sessionId: "session-1",
update: {
sessionUpdate: "usage_update",
used: 12_345,
size: 200_000,
},
} satisfies EffectAcpSchema.SessionNotification);

expect(result.events).toEqual([
{
_tag: "UsageUpdated",
used: 12_345,
size: 200_000,
rawPayload: {
sessionId: "session-1",
update: {
sessionUpdate: "usage_update",
used: 12_345,
size: 200_000,
},
},
},
]);
});

it("keeps permission request parsing compatible with loose extension payloads", () => {
const request = parsePermissionRequest({
sessionId: "session-1",
Expand Down
Loading
Loading