fix(runtime): unify standard interaction terminal states
This commit is contained in:
@@ -442,7 +442,7 @@ export class TrustedDOMRendererContext {
|
||||
const buttons: HTMLButtonElement[] = [];
|
||||
const selected = new Set<string>();
|
||||
const submit = (actionIDs: readonly string[]) => {
|
||||
const accepted = this.toolInteractions?.submit(call.call_id, { action_ids: [...actionIDs] }) ?? false;
|
||||
const accepted = this.toolInteractions?.submit(call.call_id, { action_id: actionIDs[0] }) ?? false;
|
||||
if (!accepted) {
|
||||
status.textContent = "该交互已结束或已提交";
|
||||
return;
|
||||
@@ -519,7 +519,7 @@ export class TrustedDOMRendererContext {
|
||||
const locked = this.applyCallStatus(card, status, call, record);
|
||||
for (const button of buttons) button.disabled = locked;
|
||||
approve.addEventListener("click", () => {
|
||||
if (!this.toolInteractions?.submit(call.call_id, { confirmed: true })) return this.markInteractionUnavailable(status);
|
||||
if (!this.toolInteractions?.submit(call.call_id, { approved: true })) return this.markInteractionUnavailable(status);
|
||||
this.markSubmitted(card, status, buttons);
|
||||
});
|
||||
cancel.addEventListener("click", () => {
|
||||
@@ -582,7 +582,8 @@ export class TrustedDOMRendererContext {
|
||||
const values = this.readFormValues(fields);
|
||||
const problem = validateFormValues(form, values);
|
||||
if (problem) { errors.textContent = problem; return; }
|
||||
if (!this.toolInteractions?.submit(call.call_id, { values })) return this.markInteractionUnavailable(status);
|
||||
const text = values[form.fields[0]?.id ?? ""];
|
||||
if (!this.toolInteractions?.submit(call.call_id, { text })) return this.markInteractionUnavailable(status);
|
||||
errors.textContent = "";
|
||||
this.markSubmitted(card, status, controls);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fixture from "@/runtime/app-management/golden/miniapp-sdk-v1.json";
|
||||
import { validateMiniAppManifest, type MiniAppManifestV1 } from "@/runtime/app-management/miniapp-manifest";
|
||||
import { validateStandardInteractionRequest } from "@/runtime/coordination/standard-interaction-contract";
|
||||
import { validateStandardInteractionAnswer, validateStandardInteractionRequest } from "@/runtime/coordination/standard-interaction-contract";
|
||||
|
||||
type GoldenCase = Readonly<{
|
||||
id: string;
|
||||
@@ -61,4 +61,16 @@ describe("MiniApp SDK v1 frozen golden contract", () => {
|
||||
expect(validateStandardInteractionRequest({ kind: "input", title: "描述", prompt: "请输入", expires_in_ms: 60_000, field: { id: "text", label: "描述", type: "textarea" } }, now)).toMatchObject({ accepted: true });
|
||||
expect(validateStandardInteractionRequest({ kind: "notice", title: "提示", message: "已保存", expires_in_ms: 60_000 } as never, now)).toEqual({ accepted: false, code: "interaction_request_invalid" });
|
||||
});
|
||||
|
||||
it("accepts only the frozen answer shape for each standard interaction", () => {
|
||||
const choice = { kind: "choice", title: "选择", prompt: "选一个", mode: "single-choice", actions: [{ id: "a", label: "A" }, { id: "b", label: "B" }] } as const;
|
||||
const confirm = { kind: "confirm", title: "确认", prompt: "继续吗?" } as const;
|
||||
const input = { kind: "input", title: "描述", prompt: "请输入", field: { id: "text", label: "描述", type: "textarea" } } as const;
|
||||
expect(validateStandardInteractionAnswer(choice, { action_id: "a" })).toBe(true);
|
||||
expect(validateStandardInteractionAnswer(choice, { action_ids: ["a"] })).toBe(false);
|
||||
expect(validateStandardInteractionAnswer(confirm, { approved: true })).toBe(true);
|
||||
expect(validateStandardInteractionAnswer(confirm, { confirmed: true })).toBe(false);
|
||||
expect(validateStandardInteractionAnswer(input, { text: "hello" })).toBe(true);
|
||||
expect(validateStandardInteractionAnswer(input, { values: { text: "hello" } })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
validateStandardInteractionRequest,
|
||||
validateStandardInteractionAnswer,
|
||||
type StandardInteractionRequest,
|
||||
} from "@/runtime/coordination/standard-interaction-contract";
|
||||
|
||||
@@ -95,6 +96,7 @@ export class AgentInteractionService {
|
||||
if (!record) return { disposition: "missing" };
|
||||
if (record.status !== "pending" && record.status !== "presented") return { disposition: "invalid_transition", record: copy(record) };
|
||||
if (expired(record, now)) return this.expire(interactionID, now);
|
||||
if (!validateStandardInteractionAnswer(record.request, answer)) return { disposition: "invalid_request", record: copy(record) };
|
||||
const submitted: StandardInteractionRecord = { ...record, status: "submitted", result: { outcome: "answered", answer: { ...answer } }, updated_at: now };
|
||||
const completed: StandardInteractionRecord = { ...submitted, status: "completed", updated_at: now };
|
||||
this.records.set(interactionID, completed);
|
||||
|
||||
@@ -96,6 +96,10 @@ export class InteractionKernel {
|
||||
return this.toolCalls.expireCall(callID, updatedAt);
|
||||
}
|
||||
|
||||
public setToolCallExpiry(callID: string, expiresAt: string, updatedAt: string) {
|
||||
return this.toolCalls.setExpiry(callID, expiresAt, updatedAt);
|
||||
}
|
||||
|
||||
public restoreToolCalls(records: readonly ToolCallRecord[], now: string): void {
|
||||
this.toolCalls.restore(records, now);
|
||||
}
|
||||
|
||||
@@ -172,9 +172,9 @@ describe("LineUpRuntime", () => {
|
||||
const chat = second.openChatApp();
|
||||
await second.login(login);
|
||||
expect(second.standardInteractions()).toEqual([expect.objectContaining({ call_id: "interactive-restart-1", status: "presented" })]);
|
||||
expect(chat.interactions.submit("interactive-restart-1", { confirmed: true })).toBe(true);
|
||||
expect(chat.interactions.submit("interactive-restart-1", { approved: true })).toBe(true);
|
||||
await second.flushOutbox();
|
||||
expect(second.standardInteractions()).toEqual([expect.objectContaining({ call_id: "interactive-restart-1", status: "completed", result: { outcome: "answered", answer: { confirmed: true } } })]);
|
||||
expect(second.standardInteractions()).toEqual([expect.objectContaining({ call_id: "interactive-restart-1", status: "completed", result: { outcome: "answered", answer: { approved: true } } })]);
|
||||
expect(secondTransport.sent.filter(entry => entry.payload.includes("lineup.v1.tool.result"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -212,9 +212,60 @@ describe("LineUpRuntime", () => {
|
||||
await runtime.syncOnce();
|
||||
await runtime.flushOutbox();
|
||||
expect(runtime.standardInteractions()).toEqual([expect.objectContaining({ call_id: "dismiss-question", status: "cancelled" })]);
|
||||
expect(runtime.toolCalls()).toEqual([expect.objectContaining({ call_id: "dismiss-question", status: "cancelled" })]);
|
||||
expect(transport.sent.filter(entry => entry.payload.includes("lineup.v1.tool.cancel"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects an invalid standard answer before either interaction state or outbox changes", async () => {
|
||||
const conversationID = "user_1:2:channel_1";
|
||||
const question = JSON.stringify({ v: 1, id: "answer-shape-question", type: "lineup.v1.tool.call", conversation_id: conversationID, sender: { kind: "agent", id: "agent_1" }, payload: { call_id: "answer-shape-question", tool: "choice", title: "选择", prompt: "选一个", data: { action_group: { mode: "single-choice", actions: [{ id: "a", label: "A" }, { id: "b", label: "B" }] } } } });
|
||||
const transport = new FakeTransport([[{ message_seq: 53, from_uid: "agent_1", payload: question }], []]);
|
||||
const runtime = new LineUpRuntime({ storage: new MemoryStorage(), createTransport: () => transport });
|
||||
const chat = runtime.openChatApp();
|
||||
await runtime.login(login);
|
||||
await runtime.syncOnce();
|
||||
expect(chat.interactions.submit("answer-shape-question", { action_ids: ["a"] })).toBe(false);
|
||||
expect(runtime.standardInteractions()).toEqual([expect.objectContaining({ call_id: "answer-shape-question", status: "presented" })]);
|
||||
expect(runtime.toolCalls()).toEqual([expect.objectContaining({ call_id: "answer-shape-question", status: "pending" })]);
|
||||
expect(chat.interactions.submit("answer-shape-question", { action_id: "a" })).toBe(true);
|
||||
await runtime.flushOutbox();
|
||||
expect(transport.sent.filter(entry => entry.payload.includes("lineup.v1.tool.result"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses one default expiry for Interaction and Kernel and emits one expired result", async () => {
|
||||
let now = "2026-08-04T00:00:00.000Z";
|
||||
const conversationID = "user_1:2:channel_1";
|
||||
const question = JSON.stringify({ v: 1, id: "default-expiry-question", type: "lineup.v1.tool.call", conversation_id: conversationID, sender: { kind: "agent", id: "agent_1" }, payload: { call_id: "default-expiry-question", tool: "confirm", title: "确认", prompt: "继续吗?", data: { confirm: {} } } });
|
||||
const transport = new FakeTransport([[{ message_seq: 54, from_uid: "agent_1", payload: question }], []]);
|
||||
const runtime = new LineUpRuntime({ storage: new MemoryStorage(), createTransport: () => transport, now: () => now });
|
||||
await runtime.login(login);
|
||||
await runtime.syncOnce();
|
||||
expect(runtime.standardInteractions()[0]).toMatchObject({ expires_at: "2026-08-04T00:15:00.000Z", status: "presented" });
|
||||
expect(runtime.toolCalls()[0]).toMatchObject({ request: { expires_at: "2026-08-04T00:15:00.000Z" }, status: "pending" });
|
||||
now = "2026-08-04T00:16:00.000Z";
|
||||
expect(runtime.expireToolCall("default-expiry-question")).toBe(true);
|
||||
await runtime.flushOutbox();
|
||||
expect(runtime.standardInteractions()[0]).toMatchObject({ status: "expired" });
|
||||
expect(runtime.toolCalls()[0]).toMatchObject({ status: "expired" });
|
||||
expect(transport.sent.filter(entry => entry.payload.includes("default-expiry-question") && entry.payload.includes("\"status\":\"expired\"")).length).toBe(1);
|
||||
});
|
||||
|
||||
it("replays one expired result when Runtime restarts after an unanswered interaction expires", async () => {
|
||||
const storage = new MemoryStorage();
|
||||
const conversationID = "user_1:2:channel_1";
|
||||
const question = JSON.stringify({ v: 1, id: "restart-expiry-question", type: "lineup.v1.tool.call", conversation_id: conversationID, sender: { kind: "agent", id: "agent_1" }, payload: { call_id: "restart-expiry-question", tool: "confirm", title: "确认", prompt: "继续吗?", data: { confirm: {} } } });
|
||||
const first = new LineUpRuntime({ storage, createTransport: () => new FakeTransport([[{ message_seq: 55, from_uid: "agent_1", payload: question }], []]), now: () => "2026-08-04T00:00:00.000Z" });
|
||||
await first.login(login);
|
||||
await first.syncOnce();
|
||||
const transport = new FakeTransport();
|
||||
const second = new LineUpRuntime({ storage, createTransport: () => transport, now: () => "2026-08-04T00:16:00.000Z" });
|
||||
await second.login(login);
|
||||
expect(second.standardInteractions()).toEqual([expect.objectContaining({ call_id: "restart-expiry-question", status: "expired" })]);
|
||||
expect(second.toolCalls()).toEqual([expect.objectContaining({ call_id: "restart-expiry-question", status: "expired" })]);
|
||||
await second.flushOutbox();
|
||||
expect(transport.sent.filter(entry => entry.payload.includes("restart-expiry-question") && entry.payload.includes("\"status\":\"expired\"")).length).toBe(1);
|
||||
});
|
||||
|
||||
it("delivers a revisioned ordinary Tool to bundled MiniApp SDK and keeps the App open after completion", async () => {
|
||||
const conversationID = "user_1:2:channel_1";
|
||||
const call = JSON.stringify({
|
||||
|
||||
@@ -542,9 +542,25 @@ export class LineUpRuntime {
|
||||
for (const interaction of this.interactionService.list()) {
|
||||
if (interaction.kind === "input" && (interaction.status === "pending" || interaction.status === "presented")) this.store.clearToolDraft(interaction.call_id);
|
||||
}
|
||||
this.kernel.restoreToolCalls(snapshot.tool_calls, this.now());
|
||||
const restoredInteractions = this.interactionService.list();
|
||||
const normalizedToolCalls = snapshot.tool_calls.map(call => {
|
||||
const interaction = restoredInteractions.find(record => record.call_id === call.call_id);
|
||||
return interaction?.expires_at && !call.request.expires_at
|
||||
? { ...call, request: { ...call.request, expires_at: interaction.expires_at } }
|
||||
: call;
|
||||
});
|
||||
this.kernel.restoreToolCalls(normalizedToolCalls, this.now());
|
||||
this.kernel.restoreTasks(snapshot.tasks);
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
const previouslyOpen = new Set(snapshot.standard_interactions
|
||||
.filter(record => record.agent_id === this.session.agent_uid && record.conversation_id === this.requireConversationID() && (record.status === "pending" || record.status === "presented"))
|
||||
.map(record => record.call_id));
|
||||
this.store.replaceStandardInteractions(restoredInteractions);
|
||||
for (const interaction of restoredInteractions) {
|
||||
if (interaction.status !== "expired" || !previouslyOpen.has(interaction.call_id)) continue;
|
||||
const alreadyQueued = this.store.snapshot().outbox.some(entry => entry.kind === "tool-result" && entry.payload.includes(`\"call_id\":\"${interaction.call_id}\"`) && entry.payload.includes("\"status\":\"expired\""));
|
||||
if (!alreadyQueued) void this.queueProtocolAction("tool-result", "lineup.v1.tool.result", { call_id: interaction.call_id, status: "expired", result: {} });
|
||||
}
|
||||
this.store.replaceTasks(this.kernel.snapshot().tasks);
|
||||
this.session.last_seq = snapshot.cursor;
|
||||
if (!this.lifecycle.instances.activeFor("chat", this.requireConversationID())) {
|
||||
@@ -636,14 +652,22 @@ export class LineUpRuntime {
|
||||
public submitToolCall(callID: string, result: JsonObject): boolean {
|
||||
const interaction = this.interactionService.list().find(record => record.call_id === callID && (record.status === "pending" || record.status === "presented"));
|
||||
if (interaction) {
|
||||
const kernelTransition = this.kernel.submitToolCall(callID, this.now(), result);
|
||||
if (kernelTransition.disposition !== "accepted") return false;
|
||||
const before = this.interactionService.list();
|
||||
const transition = this.interactionService.submit(interaction.interaction_id, result, this.now());
|
||||
if (transition.disposition !== "accepted") return false;
|
||||
const now = this.now();
|
||||
const kernelTransition = transition.record.status === "expired"
|
||||
? this.kernel.expireToolCall(callID, now)
|
||||
: this.kernel.submitToolCall(callID, now, result);
|
||||
if (kernelTransition.disposition !== "accepted") {
|
||||
this.interactionService.restore(before, now);
|
||||
return false;
|
||||
}
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
this.store.replaceStandardInteractions(this.interactionService.list());
|
||||
this.store.clearToolDraft(callID);
|
||||
void this.queueProtocolAction("tool-result", "lineup.v1.tool.result", { call_id: callID, status: "completed", result });
|
||||
const status = transition.record.status === "expired" ? "expired" : "completed";
|
||||
void this.queueProtocolAction("tool-result", "lineup.v1.tool.result", { call_id: callID, status, result: status === "expired" ? {} : result });
|
||||
this.emitState();
|
||||
return true;
|
||||
}
|
||||
@@ -659,10 +683,14 @@ export class LineUpRuntime {
|
||||
public cancelToolCall(callID: string, reason: string): boolean {
|
||||
const interaction = this.interactionService.list().find(record => record.call_id === callID && (record.status === "pending" || record.status === "presented"));
|
||||
if (interaction) {
|
||||
const kernelTransition = this.kernel.cancelToolCall(callID, reason, this.now());
|
||||
if (kernelTransition.disposition !== "accepted") return false;
|
||||
const before = this.interactionService.list();
|
||||
const transition = this.interactionService.dismiss({ call_id: callID, agent_id: this.session.agent_uid, conversation_id: this.requireConversationID() }, this.now());
|
||||
if (transition.disposition !== "accepted") return false;
|
||||
const kernelTransition = this.kernel.cancelToolCall(callID, reason, this.now());
|
||||
if (kernelTransition.disposition !== "accepted") {
|
||||
this.interactionService.restore(before, this.now());
|
||||
return false;
|
||||
}
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
this.store.replaceStandardInteractions(this.interactionService.list());
|
||||
this.store.clearToolDraft(callID);
|
||||
@@ -680,6 +708,24 @@ export class LineUpRuntime {
|
||||
}
|
||||
|
||||
public expireToolCall(callID: string): boolean {
|
||||
const interaction = this.interactionService.list().find(record => record.call_id === callID && (record.status === "pending" || record.status === "presented"));
|
||||
if (interaction) {
|
||||
const before = this.interactionService.list();
|
||||
const now = this.now();
|
||||
const interactionTransition = this.interactionService.expire(interaction.interaction_id, now);
|
||||
if (interactionTransition.disposition !== "accepted") return false;
|
||||
const kernelTransition = this.kernel.expireToolCall(callID, now);
|
||||
if (kernelTransition.disposition !== "accepted") {
|
||||
this.interactionService.restore(before, now);
|
||||
return false;
|
||||
}
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
this.store.replaceStandardInteractions(this.interactionService.list());
|
||||
this.store.clearToolDraft(callID);
|
||||
void this.queueProtocolAction("tool-result", "lineup.v1.tool.result", { call_id: callID, status: "expired", result: {} });
|
||||
this.emitState();
|
||||
return true;
|
||||
}
|
||||
const transition = this.kernel.expireToolCall(callID, this.now());
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
this.emitState();
|
||||
@@ -925,12 +971,20 @@ export class LineUpRuntime {
|
||||
}
|
||||
}
|
||||
if (route.disposition === "dismiss") {
|
||||
const before = this.interactionService.list();
|
||||
const transition = this.interactionService.dismiss({
|
||||
call_id: route.call_id,
|
||||
agent_id: this.session.agent_uid,
|
||||
conversation_id: this.requireConversationID(),
|
||||
}, this.now());
|
||||
if (transition.disposition === "accepted") {
|
||||
const kernelTransition = this.kernel.cancelToolCall(route.call_id, route.reason ?? "agent_dismissed", this.now());
|
||||
if (kernelTransition.disposition !== "accepted") {
|
||||
this.interactionService.restore(before, this.now());
|
||||
runtimeLog("interaction.dismiss_ignored", { call_id: route.call_id, control_id: route.control_id, disposition: kernelTransition.disposition });
|
||||
return;
|
||||
}
|
||||
this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
this.store.replaceStandardInteractions(this.interactionService.list());
|
||||
void this.queueProtocolAction("tool-cancel", "lineup.v1.tool.cancel", {
|
||||
call_id: route.call_id,
|
||||
@@ -1042,9 +1096,18 @@ export class LineUpRuntime {
|
||||
});
|
||||
if (created.disposition === "invalid_request") runtimeLog("interaction.rejected", { reason: "invalid_request" });
|
||||
if (created.disposition === "accepted") {
|
||||
if (created.record.expires_at) {
|
||||
const expiry = this.kernel.setToolCallExpiry(item.call.call_id, created.record.expires_at, receivedAt);
|
||||
if (expiry.disposition === "accepted") this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
}
|
||||
const presented = this.interactionService.present(created.record.interaction_id, receivedAt);
|
||||
this.store.replaceStandardInteractions(this.interactionService.list());
|
||||
if (presented.disposition === "accepted" && presented.record.status === "completed") {
|
||||
const submitted = this.kernel.submitToolCall(item.call.call_id, receivedAt, {});
|
||||
const completed = submitted.disposition === "accepted"
|
||||
? this.kernel.completeToolCall(item.call.call_id, "completed", {}, receivedAt)
|
||||
: submitted;
|
||||
if (completed.disposition === "accepted") this.store.replaceToolCalls(this.kernel.snapshot().tool_calls);
|
||||
void this.queueProtocolAction("tool-result", "lineup.v1.tool.result", { call_id: item.call.call_id, status: "completed", result: {} });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,24 @@ export type InteractionRequestValidation =
|
||||
| Readonly<{ accepted: true; expires_at?: string }>
|
||||
| Readonly<{ accepted: false; code: "interaction_request_invalid" }>;
|
||||
|
||||
/** Validates the only answer shape that Runtime accepts for a standard interaction. */
|
||||
export function validateStandardInteractionAnswer(request: StandardInteractionRequest, answer: unknown): answer is Readonly<Record<string, unknown>> {
|
||||
if (!isObject(answer)) return false;
|
||||
const keys = Object.keys(answer);
|
||||
if (request.kind === "choice") {
|
||||
if (keys.length !== 1 || keys[0] !== "action_id" || typeof answer.action_id !== "string") return false;
|
||||
return request.actions.some(action => action.id === answer.action_id);
|
||||
}
|
||||
if (request.kind === "confirm") return keys.length === 1 && keys[0] === "approved" && typeof answer.approved === "boolean";
|
||||
if (request.kind === "input") {
|
||||
if (keys.length !== 1 || keys[0] !== "text" || typeof answer.text !== "string") return false;
|
||||
const text = answer.text;
|
||||
if (request.field.required && text.trim().length === 0) return false;
|
||||
return text.length <= (request.field.max_length ?? 1000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Validates a request before it becomes a durable StandardInteractionRecord. */
|
||||
export function validateStandardInteractionRequest(request: StandardInteractionRequest, now: Date): InteractionRequestValidation {
|
||||
if (!validTitle(request.title)) return invalid();
|
||||
@@ -83,3 +101,8 @@ function validTitle(value: string): boolean { return validText(value) && value.l
|
||||
function validText(value: string): boolean { return typeof value === "string" && value.trim().length > 0 && value.length <= 4000; }
|
||||
function validIdentifier(value: string): boolean { return typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value); }
|
||||
function invalid(): InteractionRequestValidation { return { accepted: false, code: "interaction_request_invalid" }; }
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,15 @@ export class ToolCallStateMachine {
|
||||
return { disposition: "accepted", record: copy(record) };
|
||||
}
|
||||
|
||||
public setExpiry(callID: string, expiresAt: string, updatedAt: string): ToolCallTransition {
|
||||
const record = this.calls.get(callID);
|
||||
if (!record || record.status !== "pending" || !Number.isFinite(Date.parse(expiresAt))) return { disposition: record ? "invalid_transition" : "missing", ...(record ? { record: copy(record) } : {}) };
|
||||
if (record.request.expires_at && record.request.expires_at !== expiresAt) return { disposition: "invalid_transition", record: copy(record) };
|
||||
record.request = { ...record.request, expires_at: expiresAt };
|
||||
record.updated_at = updatedAt;
|
||||
return { disposition: "accepted", record: copy(record) };
|
||||
}
|
||||
|
||||
public expire(now: string): readonly ToolCallRecord[] {
|
||||
const changed: ToolCallRecord[] = [];
|
||||
const nowTime = Date.parse(now);
|
||||
|
||||
Reference in New Issue
Block a user