fix(chat): scope native completion contracts to the response stage

This commit is contained in:
Dotta committed 2026-10-31 16:06:49 -05:00
1 parent 3e89f82783
commit 0d8ac2855e
4 files changed
+77 -7

No files matched your search

+8
View File
@@ -2529,6 +2529,14 @@ wake from a server-verified human card response references that entry in
`humanResponses`, whose answer is already present in the current request context.
Agent/tool outcomes and generated summaries are not promoted to human direction.
Standing agent chats complete the current response stage. Each pending request
keeps its criterion identity and source, with instructions to address it within
that stage. Authorized chat actions require verified results. Delegation requires
a verified handoff and a reply; the worker's task remains separate. An unavailable
action requires a truthful limitation and supported handoff. Ordinary task
contracts still require the requested work itself, and an admitted run retains
its saved contract during recovery.
These are model instructions, not additional execution or permission gates.
Contracts reference the existing brief and answers instead of copying them again.
Resumed sessions keep the existing message-delta path; fresh sessions receive the
@@ -12,7 +12,7 @@ import { nativeRuntimeContextFixture } from "../native-runtime/runtime-context.t
import { prepareNativeWorkspaceSync } from "../native-runtime/native-workspace-sync.js";
import { resolveHeartbeatNativeRuntimeMode } from "../native-runtime/runtime-mode.js";
import { claimNativeRestartRecoveries } from "../native-runtime/native-restart-recovery.js";
import { canonicalNativeRuntimeContextDigest } from "../../vendor/paperclip-runner/index.js";
import { buildNativeModelEnvelope, canonicalNativeRuntimeContextDigest } from "../../vendor/paperclip-runner/index.js";
import { selectHeartbeatRuntime, type HeartbeatRuntimeSelectionInput } from "./runtime-selection.js";
import { legacyControllerClaim } from "../legacy-controller-lease.js";
@@ -76,6 +76,28 @@ describe.skipIf(!support.supported)("heartbeat runtime selection boundary", () =
const coordinators = (id: string) => db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, id));
const sandbox = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd: "/workspace", reusableLeaseConfigured: false };
it("persists and admits a standing-chat response contract without requiring the unavailable action", async () => {
const { input, issue, agent, run } = await fixture();
const [conversation] = await db.update(issues).set({ title: "Chat with CEO", description: null,
conversationAgentId: agent.id, conversationUserId: "runtime-test-operator", conversationState: "active" })
.where(eq(issues.id, issue.id)).returning();
input.task.issueRef = conversation!;
input.task.context.conversationMode = true;
const request = "Restart this instance.";
input.task.safeWakeComments = [{ id: randomUUID(), body: request }];
const selected = await selectHeartbeatRuntime(db, input);
if (!selected.selected || !selected.nativeExecution) throw new Error("Expected native selection");
const contract = selected.nativeExecution.completionContract.contract;
expect(contract.objective).toMatch(/current conversation response/i);
expect(contract.criteria).toEqual([{ id: "objective", requirement: expect.stringContaining(request) }]);
expect(contract.criteria[0]!.requirement).not.toBe(request);
const [persisted] = await db.select().from(completionContracts).where(eq(completionContracts.issueId, issue.id));
expect(persisted!.contractJson).toEqual(contract);
expect(selected.nativeExecution.completionContract.sha256).toBe(persisted!.canonicalSha256);
expect((await read(run.id)).runnerProfileJson?.nativeExecutionInput).toEqual(selected.nativeExecution);
expect(buildNativeModelEnvelope(selected.nativeExecution).completionContract).toEqual(contract);
});
it("keeps recovery out of live preparation before the workspace descriptor exists", async () => {
const { input, run, issue } = await fixture();
input.workspace.executionTarget = sandbox;
@@ -11,6 +11,38 @@ import {
} from "./completion-contracts.js";
describe("buildNativeCompletionContract", () => {
it("scopes standing-chat criteria to the response while preserving every current request and source", () => {
const issue = { id: "chat", title: "Chat with CEO", description: null, conversationAgentId: "ceo" };
const comments = [
{ id: "operator-request", body: "Restart this instance." },
{ id: "handoff-request", body: "Have Morgan draft the update and send it here when finished." },
];
const pending = nativeCompletionRequestsWithSources(comments);
const contract = buildNativeCompletionContract(issue, { immediateRequests: pending.requests, humanResponseId: "answer" });
expect(contract.objective).toMatch(/conversation response/i);
expect(contract.objective).toMatch(/verified|verify/i);
expect(contract.objective).toMatch(/delegated/i);
expect(contract.objective).toMatch(/unavailable/i);
expect(contract.criteria.map(row => row.id)).toEqual(["pending_comment_1", "pending_comment_2", "human_response"]);
for (const [index, comment] of comments.entries()) {
const requirement = contract.criteria[index]!.requirement;
expect(requirement).not.toBe(comment.body);
expect(requirement).toContain(comment.body);
expect(requirement).toMatch(/current authorized conversation response/i);
expect(requirement.split(comment.body)).toHaveLength(2);
}
expect(contract.criteria[2]!.requirement).toContain('"answer"');
expect(buildNativeCompletionContractSources({ issue, immediateRequests: pending.requests, immediateRequestSources: pending.sources }))
.toEqual(comments.map((comment, index) => ({ id: `pending_comment_${index + 1}`, source: nativeCompletionSource("comment", comment.id, comment.body) })));
});
it.each([undefined, null])("keeps an ordinary task's action criterion strict even with a chat-like title (%s)", (conversationAgentId) => {
const issue = { title: "Chat with CEO", description: null, conversationAgentId };
const request = "Restart this instance.";
expect(buildNativeCompletionContract(issue, { immediateRequest: request }).criteria)
.toEqual([{ id: "objective", requirement: request }]);
});
it("uses the task description as the single initial criterion", () => {
expect(buildNativeCompletionContract({
title: "Ship the runner",
@@ -87,7 +87,7 @@ export function resolveNativeCompletionPolicy(issue: {
}
export function buildNativeCompletionContract(
issue: { title: string; description: string | null },
issue: { title: string; description: string | null; conversationAgentId?: string | null },
options: {
readonly revision?: number;
readonly immediateRequest?: string | null;
@@ -104,19 +104,26 @@ export function buildNativeCompletionContract(
.filter((request) => request.length > 0);
const humanResponseId = options.humanResponseId?.trim();
const hasFollowUp = immediateRequests.length > 0 || Boolean(humanResponseId);
const conversation = Boolean(issue.conversationAgentId);
const requirement = (request: string) => conversation
? `Address this request in the current authorized conversation response: ${request}`
: request;
// Reference existing context rather than copying the brief/history into every
// follow-up contract. Keep this guidance stable across comments and resumes.
const followUpObjective = "Complete the current authorized stage using the task brief and current user direction in the supplied context. Later human direction replaces conflicting scope; preserve other requirements, assigned-skill instructions, and approval gates. Apply authenticated humanResponses only to their question or decision. Clarification is not approval. If acceptance is required, propose or save the requested plan and wait before executing.";
const conversationObjective = "Complete the current conversation response using the supplied context and current user direction. Carry out requested chat actions within current authority and verify their results before claiming success. For delegated work, verify the saved handoff and report it without waiting for or performing the worker's task. For an unavailable action, explain the specific limitation and supported handoff without claiming the action occurred. Finishing this response leaves underlying delegated or unavailable work incomplete until separately verified. Later human direction replaces conflicting scope; preserve other requirements, assigned-skill instructions, and approval gates. Apply authenticated humanResponses only to their question or decision. Clarification is not approval.";
return {
revision: String(options.revision ?? 1),
objective: hasFollowUp
? followUpObjective
: issue.title,
objective: conversation
? conversationObjective
: hasFollowUp
? followUpObjective
: issue.title,
criteria: hasFollowUp
? [
...immediateRequests.map((request, index) => ({
id: immediateRequests.length === 1 ? "objective" : `pending_comment_${index + 1}`,
requirement: request,
requirement: requirement(request),
})),
...(humanResponseId ? [{
id: "human_response",
@@ -127,7 +134,7 @@ export function buildNativeCompletionContract(
{
id: "objective",
requirement:
issue.description?.trim() || `Complete: ${issue.title}`,
requirement(issue.description?.trim() || (conversation ? "Respond to the current conversation context." : `Complete: ${issue.title}`)),
},
],
};
@@ -157,6 +164,7 @@ export async function ensureNativeCompletionContract(input: {
title: string;
description: string | null;
reviewPolicy?: string | null;
conversationAgentId?: string | null;
};
actorId: string;
immediateRequest?: string | null;