@nexface/agent 0.1.1-alpha.6 → 0.1.1-alpha.8

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.
package/dist/agent.js CHANGED
@@ -1,8 +1,8 @@
1
- import { decodeRequest, toProtocolMessages, mergeProtocolMessages, runResponse } from "./request.js";
1
+ import { decodeRequest, convertMessages, parseResume, approvalResponseSchema, toolResultResponseSchema, toProtocolMessages, mergeProtocolMessages, runResponse } from "./request.js";
2
2
  import { getInternalToolRuntime } from "@nexface/tools/internal";
3
3
  import { BROWSER_TOOLS_SKILL, serializeBrowserToolsSkill, } from "./browser-prompt.js";
4
4
  import { AgentError, isAgentError } from "./errors.js";
5
- import { EventType, TextInputContentSchema, ImageInputContentSchema, ResumeEntrySchema } from "@ag-ui/core";
5
+ import { EventType, RunAgentInputSchema } from "@ag-ui/core";
6
6
  import { agentBrand } from "./types.js";
7
7
  import { describeModelConfig, prepareModelConfig, resolveModelBinding, } from "./model/binding.js";
8
8
  import { AgentModelError } from "./model/error.js";
@@ -612,9 +612,11 @@ class AgentRunStream {
612
612
  this.#emit({
613
613
  type: EventType.RUN_FINISHED, threadId, runId: id,
614
614
  outcome: result.outcome,
615
- result: { ...(result.output === undefined ? {} : { output: result.output }), ...(result.checkpoint ? { checkpoint: result.checkpoint } : {}) },
615
+ result: { ...(result.output === undefined ? {} : { output: result.output }) },
616
616
  });
617
- return result;
617
+ return result.outcome.type === "success"
618
+ ? { outcome: result.outcome, output: result.output }
619
+ : { outcome: result.outcome };
618
620
  }
619
621
  catch (error) {
620
622
  await this.onResult?.(undefined, this.#controller.signal.aborted);
@@ -673,6 +675,8 @@ class Nexface {
673
675
  #requestPending;
674
676
  #requestResult;
675
677
  #requestUsed = false;
678
+ #hasRun = false;
679
+ #receiptQueue = [];
676
680
  #pendingModelConfig;
677
681
  #modelBinding;
678
682
  #modelInfo;
@@ -702,7 +706,7 @@ class Nexface {
702
706
  const hasToolRuntime = "toolRuntime" in options
703
707
  && options.toolRuntime !== undefined;
704
708
  if ("toolBridge" in options) {
705
- throw new AgentError("AGENT_TOOL_RUNTIME_UNAVAILABLE", "toolBridge is no longer supported. Use toolRuntime locally or ChatClient with HTTP + SSE.");
709
+ throw new AgentError("AGENT_TOOL_RUNTIME_UNAVAILABLE", "toolBridge is no longer supported. Use toolRuntime locally or NexfaceProvider with HTTP + SSE.");
706
710
  }
707
711
  if (hasToolRuntime) {
708
712
  assertToolRuntime(options.toolRuntime);
@@ -745,7 +749,12 @@ class Nexface {
745
749
  throw new Error("Conversation can only be restored into a new Agent.");
746
750
  this.#messages.push(...structuredClone(messages));
747
751
  },
748
- settle: (interruptId, settlement) => this.#settleInterrupt(interruptId, settlement),
752
+ settle: (interruptId, settlement, receipts) => {
753
+ const settled = this.#settleInterrupt(interruptId, settlement);
754
+ if (receipts)
755
+ this.#alignCancellationReceipts(receipts);
756
+ return settled;
757
+ },
749
758
  authorizePageResponse: (response) => {
750
759
  this.#authorizedPageResponses.add(response);
751
760
  },
@@ -783,114 +792,264 @@ class Nexface {
783
792
  }
784
793
  }
785
794
  run(input, options = {}) {
786
- if ("messages" in input)
787
- return this.#runRequest(input, options);
788
- if (this.#activeRun) {
795
+ this.#assertAvailable();
796
+ let messages;
797
+ let resume;
798
+ try {
799
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => key !== "messages" && key !== "resume"))
800
+ throw new Error("run accepts only messages and resume.");
801
+ if (input.messages !== undefined && !Array.isArray(input.messages))
802
+ throw new Error("messages must be an array.");
803
+ if (input.resume !== undefined && (!Array.isArray(input.resume) || input.resume.length !== 1))
804
+ throw new Error("Exactly one resume entry is required.");
805
+ resume = input.resume?.[0] ? parseResume(input.resume[0]) : undefined;
806
+ if (!input.messages?.length && !resume)
807
+ throw new Error("Provide messages or resume.");
808
+ const previous = mergeProtocolMessages(toProtocolMessages(this.#messages), this.#requestMessages);
809
+ const incomingIds = new Set(input.messages?.map((message) => message.id));
810
+ messages = convertMessages([...previous, ...input.messages ?? []]).filter((message) => incomingIds.has(message.id));
811
+ for (const message of messages)
812
+ if (message.role === "assistant")
813
+ validateToolCallIds(message.toolCalls);
814
+ if (resume) {
815
+ if (!this.#pendingInterrupt || resume.interruptId !== this.#pendingInterrupt.descriptor.id)
816
+ throw new Error("The interrupt is no longer pending.");
817
+ if (resume.status === "resolved") {
818
+ if (!("approved" in resume.payload))
819
+ throw new Error("This pending approval requires an approval response.");
820
+ if (messages.length)
821
+ throw new Error("A resolved approval cannot start a new task.");
822
+ if (!resume.payload.approved && this.#pendingInterrupt.descriptor.mode === "page")
823
+ throw new Error("A page confirmation cannot be rejected through run.");
824
+ if (resume.payload.approved && this.#pendingInterrupt.descriptor.mode === "page" && !this.#authorizedPageResponses.delete(input.resume[0]))
825
+ throw new Error("Page confirmation must be submitted through its registered usePageConfirmation Hook.");
826
+ }
827
+ }
828
+ }
829
+ catch (error) {
830
+ throw new AgentError("INVALID_RUN_INPUT", error instanceof Error ? error.message : "Invalid Agent input.");
831
+ }
832
+ return this.#start({ kind: "local", messages, protocolMessages: structuredClone(input.messages ?? []), ...(resume ? { resume } : {}) }, options);
833
+ }
834
+ runRequest(input, options = {}) {
835
+ let wire;
836
+ try {
837
+ wire = RunAgentInputSchema.parse(input);
838
+ }
839
+ catch (error) {
840
+ throw new AgentError("INVALID_RUN_INPUT", error instanceof Error ? error.message : "Invalid request shape.");
841
+ }
842
+ if (this.#hasRun || this.#activeRun || this.#modelSwitch || this.#messages.length || this.#toolRuntime)
843
+ throw new AgentError("AGENT_REQUEST_REQUIRES_NEW_AGENT", "runRequest requires a new Agent without a local ToolRuntime.");
844
+ return this.#start({ kind: "request", wire: structuredClone(wire) }, options);
845
+ }
846
+ #assertAvailable() {
847
+ if (this.#activeRun)
789
848
  throw new AgentError("AGENT_RUN_ACTIVE", "An agent run is already active.");
849
+ if (this.#modelSwitch)
850
+ throw new AgentModelError("AGENT_RUN_BLOCKED", "The model cannot be switched while the Agent is busy.");
851
+ if (this.#requestUsed)
852
+ throw new AgentError("AGENT_REQUEST_REQUIRES_NEW_AGENT", "A request Agent cannot be reused.");
853
+ }
854
+ #start(source, options) {
855
+ const runId = source.kind === "request" ? source.wire.runId : createId();
856
+ if (source.kind === "local" && !options.signal?.aborted) {
857
+ if (this.#toolRuntime && !source.resume) {
858
+ const previousOperation = this.#operation;
859
+ const abort = new AbortController();
860
+ const handle = getInternalToolRuntime(this.#toolRuntime).beginInternalOperation({
861
+ signal: abort.signal,
862
+ taskProgress: { phase: "processing" },
863
+ onCancel: () => {
864
+ abort.abort(new DOMException("Agent operation cancelled.", "AbortError"));
865
+ if (this.#operation?.abort === abort && this.#pendingInterrupt)
866
+ this.#settleInterrupt(this.#pendingInterrupt.descriptor.id, { type: "cancel" });
867
+ },
868
+ ...(previousOperation ? { replaceWaiting: previousOperation.handle } : {}),
869
+ });
870
+ this.#operation = { handle, abort };
871
+ }
872
+ if (!source.resume && this.#pendingInterrupt) {
873
+ const previous = this.#takeInterrupt(this.#pendingInterrupt.descriptor.id);
874
+ this.#appendInterruptedBatch(previous, errorPayload(new AgentToolError("TOOL_CANCELLED", "A new user message replaced the pending task.", { toolExecuted: false })));
875
+ }
876
+ else if (source.resume && this.#pendingInterrupt) {
877
+ // Transfer ownership before exposing the resumed Run to cancellation.
878
+ this.#pendingInterrupt.descriptor = Object.freeze({ ...this.#pendingInterrupt.descriptor, runId });
879
+ }
790
880
  }
791
- if (this.#modelSwitch) {
792
- throw new AgentModelError("AGENT_RUN_BLOCKED", "An agent run cannot start while the model is switching.");
881
+ if (source.kind === "request") {
882
+ this.#requestUsed = true;
883
+ this.#requestIdentity = { threadId: source.wire.threadId, runId };
793
884
  }
794
- if ("resume" in input) {
795
- if (!Array.isArray(input.resume) || input.resume.length !== 1) {
796
- throw new AgentError("INVALID_RUN_INPUT", "Nexface currently requires exactly one resume entry.");
885
+ this.#hasRun = true;
886
+ const run = this.#createRun(runId, options, async (emit, signal) => {
887
+ signal.throwIfAborted();
888
+ let incoming;
889
+ let resume;
890
+ let restored;
891
+ let requestInput;
892
+ let history = [];
893
+ if (source.kind === "request") {
894
+ try {
895
+ requestInput = decodeRequest(source.wire);
896
+ history = this.#prepareRequestHistory(requestInput);
897
+ restored = requestInput.continuation;
898
+ }
899
+ catch (error) {
900
+ throw new AgentError("INVALID_RUN_INPUT", error instanceof Error ? error.message : "Invalid request recovery.");
901
+ }
902
+ incoming = requestInput.newMessages;
903
+ resume = requestInput.resume;
797
904
  }
798
- const parsedEntry = ResumeEntrySchema.safeParse(input.resume[0]);
799
- if (!parsedEntry.success) {
800
- throw new AgentError("INVALID_RUN_INPUT", "The resume entry does not match the AG-UI interrupt response schema.");
905
+ else {
906
+ incoming = source.messages;
907
+ resume = source.resume;
801
908
  }
802
- const entry = parsedEntry.data;
803
- if (entry.status === "cancelled") {
804
- throw new AgentError("INVALID_RUN_INPUT", "Cancelling a pending interrupt does not create an Agent Run.");
909
+ const binding = this.#modelBinding ?? await resolveModelBinding(this.#pendingModelConfig, signal);
910
+ signal.throwIfAborted();
911
+ if (restored?.replay !== undefined) {
912
+ if (!binding.restoreReplay)
913
+ throw new AgentError("INVALID_RUN_INPUT", "The model cannot restore native replay data.");
914
+ binding.restoreReplay(restored.replay, history);
805
915
  }
806
- if (typeof entry.payload?.approved !== "boolean") {
807
- throw new AgentError("INVALID_RUN_INPUT", "Approval response requires a boolean approved field.");
916
+ // Commit only after recovery and native replay have both been validated.
917
+ this.#modelBinding = binding;
918
+ this.#modelInfo = binding.info;
919
+ this.#pendingModelConfig = undefined;
920
+ if (requestInput && source.kind === "request") {
921
+ this.#request = requestInput;
922
+ this.#requestMessages = source.wire.messages;
923
+ this.#receiptQueue = [...requestInput.receipts];
924
+ this.#messages.push(...history);
925
+ for (const skill of restored?.loadedSkills ?? [])
926
+ this.#loadedSkills.add(skill);
927
+ this.#browserSkillLoaded = restored?.browserSkillLoaded ?? false;
928
+ this.#contextRevision = restored?.contextRevision ?? -1;
929
+ this.#lastContextSerialized = restored?.lastContextSerialized;
930
+ const pending = restored?.pending;
931
+ if (pending) {
932
+ this.#requestPending = structuredClone(pending);
933
+ this.#requestResult = requestInput.toolResult;
934
+ }
808
935
  }
809
- const pending = this.#pendingInterrupt;
810
- if (entry.payload.approved
811
- && pending?.descriptor.id === entry.interruptId
812
- && pending.descriptor.mode === "page"
813
- && !this.#authorizedPageResponses.delete(input.resume[0])) {
814
- throw new AgentError("INVALID_RUN_INPUT", "Page confirmation must be submitted through its registered usePageConfirmation Hook.");
936
+ const rememberLocalInput = () => {
937
+ if (source.kind === "local")
938
+ this.#requestMessages = [...mergeProtocolMessages(toProtocolMessages(this.#messages), this.#requestMessages), ...source.protocolMessages];
939
+ };
940
+ if (resume?.status === "cancelled") {
941
+ if (this.#pendingInterrupt) {
942
+ const pending = this.#takeInterrupt(resume.interruptId);
943
+ this.#appendInterruptedBatch(pending, errorPayload(new AgentToolError("TOOL_CANCELLED", "The pending task was cancelled.", { toolExecuted: false })), emit);
944
+ }
945
+ else if (this.#requestPending) {
946
+ this.#cancelRequestPending(emit);
947
+ }
948
+ if (!incoming.length) {
949
+ rememberLocalInput();
950
+ return { outcome: { type: "success" }, output: "" };
951
+ }
815
952
  }
816
- const continuation = this.#resolveInterrupt(entry.interruptId, entry.payload.approved ? "confirm" : "reject", options);
817
- if (!continuation) {
818
- throw new AgentError("INVALID_RUN_INPUT", "The interrupt is no longer pending or cannot accept this response.");
953
+ else if (resume?.status === "resolved" && "approved" in resume.payload) {
954
+ rememberLocalInput();
955
+ return this.#continueApproval(resume.interruptId, resume.payload.approved ? "confirm" : "reject", emit, signal, runId);
819
956
  }
820
- return continuation;
821
- }
822
- const raw = input.message.content;
823
- const content = typeof raw === "string" ? raw.trim() : raw.map((part) => part.type === "text" ? TextInputContentSchema.parse(part) : ImageInputContentSchema.parse(part));
824
- if (!content.length) {
825
- throw new AgentError("INVALID_RUN_INPUT", "Agent message content is required.");
826
- }
827
- if (options.signal?.aborted) {
828
- return new AgentRunStream(createId(), this.id, options, async (_emit, signal) => {
829
- signal.throwIfAborted();
830
- throw new Error("An aborted Run cannot execute.");
831
- });
832
- }
833
- if (this.#toolRuntime) {
834
- const abort = new AbortController();
835
- const handle = getInternalToolRuntime(this.#toolRuntime).beginInternalOperation({
836
- signal: abort.signal,
837
- taskProgress: { phase: "processing" },
838
- onCancel: () => {
839
- abort.abort(new DOMException("Agent operation cancelled.", "AbortError"));
840
- if (this.#operation?.abort === abort && this.#pendingInterrupt) {
841
- this.#settleInterrupt(this.#pendingInterrupt.descriptor.id, { type: "cancel" });
842
- }
843
- },
844
- ...(this.#pendingInterrupt && this.#operation ? { replaceWaiting: this.#operation.handle } : {}),
845
- });
846
- // The old handle has been replaced atomically; invalidation cannot close the new one.
847
- this.#operation = undefined;
848
- if (this.#pendingInterrupt)
849
- this.#invalidateInterrupt("A new user message started.", "new_message");
850
- this.#operation = { handle, abort };
851
- }
852
- else if (this.#pendingInterrupt) {
853
- this.#invalidateInterrupt("A new user message started.", "new_message");
854
- }
855
- const inputMessage = { id: input.message.id ?? createId(), role: "user", content };
856
- const runId = createId();
857
- const pendingModelConfig = this.#pendingModelConfig;
858
- const existingBinding = this.#modelBinding;
859
- const run = this.#createRun(runId, options, async (emit, signal) => {
957
+ rememberLocalInput();
958
+ const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
959
+ this.#appendContextCheckpoint(contexts);
960
+ const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
961
+ appendRuntimeCatalogCheckpoint(this.#messages, definitions.filter(({ name }) => !NATIVE_TOOL_NAMES.has(name)));
962
+ const previousLength = this.#messages.length;
963
+ this.#messages.push(...structuredClone(incoming));
860
964
  try {
861
- const binding = existingBinding ?? await resolveModelBinding(pendingModelConfig, signal);
862
- signal.throwIfAborted();
863
- if (!this.#modelBinding && this.#pendingModelConfig === pendingModelConfig) {
864
- this.#modelBinding = binding;
865
- this.#modelInfo = binding.info;
866
- this.#pendingModelConfig = undefined;
867
- }
868
- const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
869
- this.#appendContextCheckpoint(contexts);
870
- const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
871
- const applicationDefinitions = definitions
872
- .filter(({ name }) => !NATIVE_TOOL_NAMES.has(name));
873
- appendRuntimeCatalogCheckpoint(this.#messages, applicationDefinitions);
874
- this.#messages.push(inputMessage);
875
- return await this.#execute(emit, signal, binding, definitions, runId);
965
+ const pending = this.#requestPending;
966
+ return await this.#execute(emit, signal, binding, definitions, runId, pending ? { step: pending.step, recoverableFailures: pending.recoverableFailures, unresolvedToolFailures: new Set(pending.unresolvedToolFailures) } : undefined);
876
967
  }
877
968
  catch (error) {
878
- // Preserve history after a Tool may have caused application side effects.
879
- if (this.#messages.at(-1) === inputMessage)
880
- this.#messages.pop();
969
+ // Roll back untouched input, but keep history after a tool may have executed.
970
+ if (this.#messages.length === previousLength + incoming.length)
971
+ this.#messages.splice(previousLength);
881
972
  throw error;
882
973
  }
883
974
  });
884
975
  this.#activeRun = run;
885
- void run.result.then(() => {
886
- if (this.#activeRun === run)
887
- this.#activeRun = undefined;
888
- }, () => {
889
- if (this.#activeRun === run)
890
- this.#activeRun = undefined;
891
- });
976
+ if (source.kind === "local" && source.resume)
977
+ this.#operation?.handle.setWaitingForUser(false);
978
+ void run.result.then(() => { if (this.#activeRun === run)
979
+ this.#activeRun = undefined; }, () => { if (this.#activeRun === run)
980
+ this.#activeRun = undefined; });
892
981
  return run;
893
982
  }
983
+ #prepareRequestHistory(input) {
984
+ validateBridgeTools(input.tools);
985
+ validateBridgeContexts(input.contexts);
986
+ const definitions = [...input.tools, ...[...this.#registered.values()].map((tool) => tool.definition)];
987
+ validateBridgeTools(definitions);
988
+ const restored = input.continuation;
989
+ if (restored && (JSON.stringify(restored.model) !== JSON.stringify(this.model) || !Array.isArray(restored.loadedSkills) || !Number.isInteger(restored.contextRevision) || typeof restored.browserSkillLoaded !== "boolean"))
990
+ throw new Error("Invalid Agent state.");
991
+ for (const name of restored?.loadedSkills ?? [])
992
+ if (!this.#skillsByName.has(name))
993
+ throw new Error("A restored Skill is unavailable.");
994
+ const catalog = serializeRuntimeCatalog(definitions.filter(({ name }) => !NATIVE_TOOL_NAMES.has(name)));
995
+ const history = input.messages.map((message) => {
996
+ if (message.role !== "system") {
997
+ if (message.role === "assistant")
998
+ validateToolCallIds(message.toolCalls);
999
+ return structuredClone(message);
1000
+ }
1001
+ const skill = message.content.startsWith("nexface:skill:") ? this.#skillsByName.get(message.content.slice(14)) : undefined;
1002
+ const content = skill ? serializeSkill(skill) : message.content === "nexface:browser" ? serializeBrowserToolsSkill() : message.content === "nexface:catalog" ? catalog : undefined;
1003
+ if (!content)
1004
+ throw new Error("Untrusted system message.");
1005
+ return { role: "system", content };
1006
+ });
1007
+ for (const message of input.newMessages)
1008
+ if (message.role === "assistant")
1009
+ validateToolCallIds(message.toolCalls);
1010
+ const pending = restored?.pending;
1011
+ if (pending) {
1012
+ if (!Number.isInteger(pending.step) || pending.step < 0 || !Number.isInteger(pending.recoverableFailures) || pending.recoverableFailures < 0 || !Array.isArray(pending.unresolvedToolFailures) || pending.unresolvedToolFailures.some((name) => typeof name !== "string") || typeof pending.browserSkillAvailable !== "boolean" || typeof pending.loadBrowserSkill !== "boolean")
1013
+ throw new Error("Invalid pending budget.");
1014
+ validateToolCallIds(pending.calls);
1015
+ const batchIndex = history.reduce((last, message, index) => message.role === "assistant" ? index : last, -1);
1016
+ const batch = history[batchIndex];
1017
+ if (batch?.role !== "assistant")
1018
+ throw new Error("Pending call has no assistant message.");
1019
+ const firstIndex = batch.toolCalls.findIndex((call) => call.id === pending.calls[0].id);
1020
+ if (firstIndex < 0 || JSON.stringify(batch.toolCalls.slice(firstIndex)) !== JSON.stringify(pending.calls))
1021
+ throw new Error("Pending calls do not match their original batch.");
1022
+ if (history.slice(batchIndex + 1).some((message) => message.role === "tool" && pending.calls.some((call) => call.id === message.toolCallId)))
1023
+ throw new Error("Pending call already has a result.");
1024
+ const first = pending.calls[0];
1025
+ const prepared = prepareToolCall(first, new Set(NATIVE_TOOL_NAMES), this.#skills.length > 0);
1026
+ if (prepared.preparationError)
1027
+ throw prepared.preparationError;
1028
+ // Client receipts settle the original call even if its Binding has since changed.
1029
+ const definition = validateBridgeTools([pending.definition])[0];
1030
+ if (!definition || definition.name !== prepared.displayName)
1031
+ throw new Error("Pending tool definition changed.");
1032
+ const pendingInput = prepared.kind === "application" ? prepared.input : Object.fromEntries(Object.entries(parseArguments(first)).filter(([key]) => key !== "progressLabel"));
1033
+ const inputErrors = validateToolInput(definition.inputSchema, pendingInput);
1034
+ if (inputErrors.length)
1035
+ throw new Error(`Invalid pending tool input: ${inputErrors.join(" ")}`);
1036
+ if (this.#registered.has(prepared.displayName))
1037
+ throw new Error("A client result cannot settle a registered tool.");
1038
+ }
1039
+ return history;
1040
+ }
1041
+ #cancelRequestPending(emit) {
1042
+ const pending = this.#requestPending;
1043
+ this.#requestPending = undefined;
1044
+ this.#requestResult = undefined;
1045
+ for (const [index, call] of pending.calls.entries()) {
1046
+ const prepared = prepareToolCall(call, new Set(NATIVE_TOOL_NAMES), this.#skills.length > 0);
1047
+ const payload = errorPayload(new AgentToolError(index === 0 ? "TOOL_CANCELLED" : "TOOL_SKIPPED", "The pending task was cancelled.", { toolExecuted: false }));
1048
+ this.#messages.push({ role: "tool", toolCallId: call.id, name: call.name, content: modelToolPayload(prepared, payload) });
1049
+ emit({ type: EventType.TOOL_CALL_RESULT, messageId: createId(), role: "tool", toolCallId: call.id, content: serializeToolPayload(payload) });
1050
+ }
1051
+ this.#loadPendingBrowserSkill(pending);
1052
+ }
894
1053
  async #executeRegistered(tool, input, toolCallId, progressLabel, emit, signal) {
895
1054
  const progress = { phase: "tool", toolCallId, ...(progressLabel ? { progressLabel } : {}) };
896
1055
  const runtime = this.#toolRuntime && getInternalToolRuntime(this.#toolRuntime);
@@ -906,7 +1065,7 @@ class Nexface {
906
1065
  emit({ type: EventType.CUSTOM, name: "nexface.tool-execution", value: { toolCallId, status: "finished" } });
907
1066
  }
908
1067
  }
909
- #checkpoint(binding) {
1068
+ #exportState(binding) {
910
1069
  toProtocolMessages(this.#messages);
911
1070
  const messages = this.#messages.map((message) => {
912
1071
  if (message.role !== "system")
@@ -920,113 +1079,12 @@ class Nexface {
920
1079
  return { role: "system", content: "nexface:catalog" };
921
1080
  });
922
1081
  const continuation = {
923
- version: 1, timeline: messages.map((message) => message.role === "system" || message.role === "application_context" ? { internal: message } : { messageId: message.id }), model: this.model, loadedSkills: [...this.#loadedSkills], browserSkillLoaded: this.#browserSkillLoaded,
1082
+ version: 2, threadId: this.#requestIdentity.threadId, timeline: messages.map((message) => message.role === "system" || message.role === "application_context" ? { internal: message } : { messageId: message.id }), model: this.model, loadedSkills: [...this.#loadedSkills], browserSkillLoaded: this.#browserSkillLoaded,
924
1083
  contextRevision: this.#contextRevision, ...(this.#lastContextSerialized === undefined ? {} : { lastContextSerialized: this.#lastContextSerialized }),
925
1084
  ...(binding.exportReplay ? { replay: binding.exportReplay(this.#messages) } : {}),
926
1085
  ...(this.#requestPending ? { pending: this.#requestPending } : {}),
927
1086
  };
928
- return { continuation: JSON.stringify(continuation) };
929
- }
930
- #runRequest(wire, options) {
931
- let input;
932
- try {
933
- input = decodeRequest(wire);
934
- }
935
- catch (error) {
936
- throw new AgentError("INVALID_RUN_INPUT", error instanceof Error ? error.message : "Invalid request.");
937
- }
938
- this.#requestIdentity = { threadId: wire.threadId, runId: wire.runId };
939
- this.#requestMessages = wire.messages;
940
- if (this.#requestUsed || this.#activeRun || this.#messages.length || this.#toolRuntime)
941
- throw new AgentError("INVALID_RUN_INPUT", "Request input requires a new server Agent.");
942
- if (!Array.isArray(input.messages) || !Array.isArray(input.tools) || !Array.isArray(input.contexts))
943
- throw new AgentError("INVALID_RUN_INPUT", "Invalid request input.");
944
- validateBridgeTools(input.tools);
945
- validateBridgeContexts(input.contexts);
946
- this.#requestUsed = true;
947
- this.#request = structuredClone(input);
948
- let continuation;
949
- try {
950
- continuation = input.continuation ? JSON.parse(input.continuation) : undefined;
951
- if (continuation && (continuation.version !== 1 || JSON.stringify(continuation.model) !== JSON.stringify(this.model) || !Array.isArray(continuation.loadedSkills) || !Number.isInteger(continuation.contextRevision)))
952
- throw new Error("Invalid continuation.");
953
- if (continuation?.pending) {
954
- const pending = continuation.pending;
955
- if (!Array.isArray(pending.calls) || !pending.calls.length || !Number.isInteger(pending.step) || pending.step < 0 || !Number.isInteger(pending.recoverableFailures) || pending.recoverableFailures < 0 || !Array.isArray(pending.unresolvedToolFailures))
956
- throw new Error("Invalid pending batch.");
957
- validateToolCallIds(pending.calls);
958
- if (input.toolResult?.toolCallId !== pending.calls[0]?.id)
959
- throw new Error("Missing or mismatched browser result.");
960
- const batchIndex = input.messages.reduce((last, message, index) => message.role === "assistant" ? index : last, -1);
961
- const batch = input.messages[batchIndex];
962
- const outstanding = batch?.role === "assistant" ? batch.toolCalls : [];
963
- for (const call of pending.calls) {
964
- if (!outstanding.some((item) => item.id === call.id && item.name === call.name && item.arguments === call.arguments) || input.messages.slice(batchIndex + 1).some((message) => message.role === "tool" && message.toolCallId === call.id))
965
- throw new Error("Pending call does not match history.");
966
- }
967
- const first = pending.calls[0];
968
- const target = first.name === "callAppTool" ? JSON.parse(first.arguments).name : first.name;
969
- if (target && this.#registered.has(target))
970
- throw new Error("A browser result cannot settle a server tool.");
971
- if (typeof input.toolResult?.confirmed !== "undefined" && typeof input.toolResult.confirmed !== "boolean")
972
- throw new Error("Invalid confirmation result.");
973
- if (input.toolResult?.error && (typeof input.toolResult.error.code !== "string" || typeof input.toolResult.error.message !== "string"))
974
- throw new Error("Invalid browser error.");
975
- this.#requestPending = structuredClone(pending);
976
- this.#requestResult = structuredClone(input.toolResult);
977
- }
978
- else if (input.toolResult)
979
- throw new Error("Unexpected browser result.");
980
- for (const name of continuation?.loadedSkills ?? []) {
981
- if (!this.#skillsByName.has(name))
982
- throw new Error("A restored Skill is unavailable.");
983
- this.#loadedSkills.add(name);
984
- }
985
- this.#browserSkillLoaded = continuation?.browserSkillLoaded ?? false;
986
- this.#contextRevision = continuation?.contextRevision ?? -1;
987
- this.#lastContextSerialized = continuation?.lastContextSerialized;
988
- const catalog = serializeRuntimeCatalog([...input.tools, ...[...this.#registered.values()].map((tool) => tool.definition)].filter((tool) => !NATIVE_TOOL_NAMES.has(tool.name)));
989
- for (const message of input.messages) {
990
- if (!message || !["system", "user", "assistant", "tool", "application_context"].includes(message.role))
991
- throw new Error("Invalid history message.");
992
- if (message.role === "system") {
993
- const skill = message.content.startsWith("nexface:skill:") ? this.#skillsByName.get(message.content.slice(14)) : undefined;
994
- const content = skill ? serializeSkill(skill) : message.content === "nexface:browser" ? serializeBrowserToolsSkill() : message.content === "nexface:catalog" ? catalog : undefined;
995
- if (!content)
996
- throw new Error("Untrusted system message.");
997
- this.#messages.push({ role: "system", content });
998
- }
999
- else {
1000
- if (message.role === "assistant")
1001
- validateToolCallIds(message.toolCalls);
1002
- if (message.role === "user" && typeof message.content !== "string") {
1003
- if (!Array.isArray(message.content))
1004
- throw new Error("Invalid user content.");
1005
- message.content.forEach((part) => part.type === "text" ? TextInputContentSchema.parse(part) : ImageInputContentSchema.parse(part));
1006
- }
1007
- else if (message.role !== "assistant" && typeof message.content !== "string")
1008
- throw new Error("Invalid history content.");
1009
- this.#messages.push(structuredClone(message));
1010
- }
1011
- }
1012
- }
1013
- catch (error) {
1014
- throw new AgentError("INVALID_RUN_INPUT", error instanceof Error ? error.message : "Invalid request.");
1015
- }
1016
- const run = this.#createRun(wire.runId, options, async (emit, signal) => {
1017
- const binding = await resolveModelBinding(this.#pendingModelConfig, signal);
1018
- this.#modelBinding = binding;
1019
- if (continuation?.replay !== undefined)
1020
- binding.restoreReplay?.(continuation.replay, this.#messages);
1021
- this.#appendContextCheckpoint(validateBridgeContexts(input.contexts));
1022
- const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
1023
- const pending = this.#requestPending;
1024
- const result = await this.#execute(emit, signal, binding, definitions, run.id, pending ? { step: pending.step, recoverableFailures: pending.recoverableFailures, unresolvedToolFailures: new Set(pending.unresolvedToolFailures) } : undefined);
1025
- return result.outcome.type === "success" ? { ...result, checkpoint: this.#checkpoint(binding) } : result;
1026
- });
1027
- this.#activeRun = run;
1028
- void run.result.then(() => { this.#activeRun = undefined; }, () => { this.#activeRun = undefined; });
1029
- return run;
1087
+ return structuredClone({ nexface: continuation });
1030
1088
  }
1031
1089
  #createRun(id, options, execute) {
1032
1090
  const operation = this.#operation;
@@ -1034,7 +1092,6 @@ class Nexface {
1034
1092
  ...options,
1035
1093
  signal: AbortSignal.any([operation.abort.signal, ...(options.signal ? [options.signal] : [])]),
1036
1094
  } : options;
1037
- let resumedResultId = this.#request?.toolResultMessageId;
1038
1095
  return new AgentRunStream(id, this.#requestIdentity?.threadId ?? this.id, runOptions, async (emit, signal) => {
1039
1096
  const onAbort = () => operation?.abort.abort(signal.reason);
1040
1097
  if (signal.aborted)
@@ -1044,18 +1101,26 @@ class Nexface {
1044
1101
  try {
1045
1102
  const result = await execute((event) => {
1046
1103
  if (event.type === EventType.TOOL_CALL_RESULT) {
1047
- const previous = this.#requestMessages.find((item) => item.id === resumedResultId && item.role === "tool" && item.toolCallId === event.toolCallId);
1048
- if (previous) {
1049
- event = { ...event, messageId: previous.id };
1050
- resumedResultId = undefined;
1051
- }
1104
+ const receiptIndex = this.#receiptQueue.findIndex((item) => item.role === "tool" && item.toolCallId === event.toolCallId);
1105
+ const previous = receiptIndex < 0 ? undefined : this.#receiptQueue.splice(receiptIndex, 1)[0];
1106
+ if (previous?.role === "tool")
1107
+ event = { ...event, messageId: previous.id, ...(this.#request?.resume?.status === "cancelled" ? { content: previous.content } : {}) };
1052
1108
  const message = this.#messages.at(-1);
1053
- if (message?.role === "tool" && message.toolCallId === event.toolCallId)
1109
+ if (message?.role === "tool" && message.toolCallId === event.toolCallId) {
1054
1110
  message.id = event.messageId;
1111
+ if (previous?.role === "tool" && this.#request?.resume?.status === "cancelled") {
1112
+ const original = JSON.parse(message.content);
1113
+ message.content = JSON.stringify({ ...(original.toolName ? { toolName: original.toolName } : {}), ...JSON.parse(previous.content) });
1114
+ }
1115
+ }
1055
1116
  }
1056
1117
  emit(event);
1057
1118
  }, signal);
1058
- emit({ type: EventType.MESSAGES_SNAPSHOT, messages: mergeProtocolMessages(toProtocolMessages(this.#messages), this.#requestMessages) });
1119
+ const messages = mergeProtocolMessages(toProtocolMessages(this.#messages), this.#requestMessages);
1120
+ this.#requestMessages = structuredClone(messages);
1121
+ emit({ type: EventType.MESSAGES_SNAPSHOT, messages });
1122
+ if (this.#requestIdentity)
1123
+ emit({ type: EventType.STATE_SNAPSHOT, snapshot: this.#exportState(this.#modelBinding) });
1059
1124
  return result;
1060
1125
  }
1061
1126
  finally {
@@ -1363,8 +1428,8 @@ class Nexface {
1363
1428
  throw new AgentToolError("INVALID_INPUT", inputErrors.join(" "), { errors: inputErrors });
1364
1429
  }
1365
1430
  if (this.#request && !registered) {
1366
- this.#requestPending = { interruptId: createId(), calls: preparedCalls.slice(index).map((item) => item.call), step, recoverableFailures, unresolvedToolFailures: [...unresolvedToolFailures], browserSkillAvailable: browserSkillAvailableForRequest, loadBrowserSkill: shouldLoadBrowserSkill };
1367
- return { outcome: { type: "interrupt", interrupts: [{ id: this.#requestPending.interruptId, reason: "tool_call", toolCallId: prepared.call.id, metadata: { nexface: { kind: "client-tool-execution", action: { toolCallId: prepared.call.id, definition: expectedDefinition, input: toolInput, ...(prepared.progressLabel ? { progressLabel: prepared.progressLabel } : {}) } } } }] }, checkpoint: this.#checkpoint(binding) };
1431
+ this.#requestPending = { kind: "client-tool-execution", definition: expectedDefinition, interruptId: createId(), calls: preparedCalls.slice(index).map((item) => item.call), step, recoverableFailures, unresolvedToolFailures: [...unresolvedToolFailures], browserSkillAvailable: browserSkillAvailableForRequest, loadBrowserSkill: shouldLoadBrowserSkill };
1432
+ return { outcome: { type: "interrupt", interrupts: [{ id: this.#requestPending.interruptId, reason: "tool_call", toolCallId: prepared.call.id, responseSchema: toolResultResponseSchema, metadata: { nexface: { kind: "client-tool-execution", action: { toolCallId: prepared.call.id, definition: expectedDefinition, input: toolInput, ...(prepared.progressLabel ? { progressLabel: prepared.progressLabel } : {}) } } } }] } };
1368
1433
  }
1369
1434
  const confirmation = runtimeTool?.confirmation;
1370
1435
  if (runtimeTool && confirmation) {
@@ -1403,6 +1468,7 @@ class Nexface {
1403
1468
  remaining: preparedCalls.slice(index + 1),
1404
1469
  modelBinding: binding,
1405
1470
  nextStep: step + 1,
1471
+ loadBrowserSkill: shouldLoadBrowserSkill,
1406
1472
  recoverableFailures,
1407
1473
  unresolvedToolFailures: new Set(unresolvedToolFailures),
1408
1474
  removeInvalidationListener: () => { },
@@ -1415,7 +1481,7 @@ class Nexface {
1415
1481
  runtimeTool.lifecycleSignal.addEventListener("abort", onRuntimeToolDisposed, { once: true });
1416
1482
  pending.removeInvalidationListener = () => runtimeTool.lifecycleSignal.removeEventListener("abort", onRuntimeToolDisposed);
1417
1483
  this.#pendingInterrupt = pending;
1418
- return { outcome: { type: "interrupt", interrupts: [{ id: descriptor.id, reason: "tool_call", toolCallId: descriptor.toolCallId, metadata: { nexface: { kind: "tool-approval", approval: descriptor } } }] } };
1484
+ return { outcome: { type: "interrupt", interrupts: [{ id: descriptor.id, reason: "tool_call", toolCallId: descriptor.toolCallId, responseSchema: approvalResponseSchema, metadata: { nexface: { kind: "tool-approval", source: "runtime", approval: descriptor } } }] } };
1419
1485
  }
1420
1486
  output = registered ? await this.#executeRegistered(registered, toolInput, prepared.call.id, prepared.progressLabel, emit, signal) : runtimeTool && this.#toolRuntime
1421
1487
  ? await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(runtimeTool, toolInput, { signal, ...(prepared.progressLabel ? { progressLabel: prepared.progressLabel } : {}) })
@@ -1441,8 +1507,8 @@ class Nexface {
1441
1507
  const inputErrors = validateToolInput(expectedDefinition.inputSchema, toolInput);
1442
1508
  if (inputErrors.length)
1443
1509
  throw new AgentToolError("INVALID_INPUT", inputErrors.join(" "));
1444
- this.#requestPending = { interruptId: createId(), calls: preparedCalls.slice(index).map((item) => item.call), step, recoverableFailures, unresolvedToolFailures: [...unresolvedToolFailures], browserSkillAvailable: browserSkillAvailableForRequest, loadBrowserSkill: shouldLoadBrowserSkill };
1445
- return { outcome: { type: "interrupt", interrupts: [{ id: this.#requestPending.interruptId, reason: "tool_call", toolCallId: prepared.call.id, metadata: { nexface: { kind: "client-tool-execution", action: { toolCallId: prepared.call.id, definition: expectedDefinition, input: toolInput, ...(progressLabel ? { progressLabel } : {}) } } } }] }, checkpoint: this.#checkpoint(binding) };
1510
+ this.#requestPending = { kind: "client-tool-execution", definition: expectedDefinition, interruptId: createId(), calls: preparedCalls.slice(index).map((item) => item.call), step, recoverableFailures, unresolvedToolFailures: [...unresolvedToolFailures], browserSkillAvailable: browserSkillAvailableForRequest, loadBrowserSkill: shouldLoadBrowserSkill };
1511
+ return { outcome: { type: "interrupt", interrupts: [{ id: this.#requestPending.interruptId, reason: "tool_call", toolCallId: prepared.call.id, responseSchema: toolResultResponseSchema, metadata: { nexface: { kind: "client-tool-execution", action: { toolCallId: prepared.call.id, definition: expectedDefinition, input: toolInput, ...(progressLabel ? { progressLabel } : {}) } } } }] } };
1446
1512
  }
1447
1513
  const nativeRuntimeTool = this.#toolRuntime?.getTools().find((tool) => tool.definition === expectedDefinition);
1448
1514
  output = nativeRuntimeTool && this.#toolRuntime
@@ -1591,6 +1657,37 @@ class Nexface {
1591
1657
  });
1592
1658
  }
1593
1659
  }
1660
+ this.#loadPendingBrowserSkill(pending);
1661
+ }
1662
+ #loadPendingBrowserSkill(pending) {
1663
+ if (pending.loadBrowserSkill && !this.#browserSkillLoaded) {
1664
+ this.#messages.push({ role: "system", content: serializeBrowserToolsSkill() });
1665
+ this.#browserSkillLoaded = true;
1666
+ }
1667
+ }
1668
+ #alignCancellationReceipts(protocol) {
1669
+ const ownerIndex = protocol.reduce((last, message, index) => message.role === "assistant" && message.toolCalls?.length ? index : last, -1);
1670
+ const owner = protocol[ownerIndex];
1671
+ if (!owner)
1672
+ return;
1673
+ const historyIndex = this.#messages.findIndex((message) => message.id === owner.id && message.role === "assistant");
1674
+ if (historyIndex < 0)
1675
+ return;
1676
+ const codes = new Set(["TOOL_CANCELLED", "TOOL_CONFIRMATION_INVALIDATED", "TOOL_SKIPPED"]);
1677
+ for (const receipt of protocol.slice(ownerIndex + 1)) {
1678
+ if (receipt.role === "user" || (receipt.role === "assistant" && receipt.toolCalls?.length))
1679
+ break;
1680
+ if (receipt.role !== "tool")
1681
+ continue;
1682
+ const current = this.#messages.slice(historyIndex + 1).find((message) => message.role === "tool" && message.toolCallId === receipt.toolCallId);
1683
+ if (current?.role !== "tool")
1684
+ continue;
1685
+ try {
1686
+ if (codes.has(JSON.parse(current.content)?.error?.code) && codes.has(JSON.parse(receipt.content)?.error?.code))
1687
+ current.id = receipt.id;
1688
+ }
1689
+ catch { /* Only known cancellation receipts are aligned. */ }
1690
+ }
1594
1691
  }
1595
1692
  #invalidateInterrupt(message, reason) {
1596
1693
  const pending = this.#pendingInterrupt;
@@ -1615,103 +1712,85 @@ class Nexface {
1615
1712
  this.#appendInterruptedBatch(pending, errorPayload(new AgentToolError("TOOL_CONFIRMATION_INVALIDATED", settlement.message, { reason: settlement.reason, toolExecuted: false })));
1616
1713
  return true;
1617
1714
  }
1618
- #resolveInterrupt(id, decision, options = {}) {
1619
- const current = this.#pendingInterrupt;
1620
- if (!current || current.descriptor.id !== id)
1621
- return undefined;
1622
- if (decision === "reject" && current.descriptor.mode !== "chat") {
1623
- return undefined;
1624
- }
1625
- options.signal?.throwIfAborted();
1715
+ async #continueApproval(id, decision, emit, signal, runId) {
1626
1716
  const pending = this.#takeInterrupt(id);
1627
1717
  if (!pending)
1628
- return undefined;
1718
+ throw new AgentError("INVALID_RUN_INPUT", "The interrupt is no longer pending.");
1629
1719
  this.#operation?.handle.setWaitingForUser(false);
1630
- const runId = createId();
1631
- const run = this.#createRun(runId, options, async (emit, signal) => {
1632
- let currentFailed = decision === "reject";
1633
- if (decision === "reject") {
1634
- this.#appendInterruptedBatch(pending, errorPayload(new AgentToolError("TOOL_CONFIRMATION_REJECTED", `Tool "${pending.descriptor.toolName}" was rejected by the user.`, { toolExecuted: false })), emit);
1720
+ let currentFailed = decision === "reject";
1721
+ if (decision === "reject") {
1722
+ this.#appendInterruptedBatch(pending, errorPayload(new AgentToolError("TOOL_CONFIRMATION_REJECTED", `Tool "${pending.descriptor.toolName}" was rejected by the user.`, { toolExecuted: false })), emit);
1723
+ }
1724
+ else {
1725
+ let payload;
1726
+ try {
1727
+ const output = await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(pending.runtimeTool, pending.input, { signal, ...(pending.prepared.progressLabel ? { progressLabel: pending.prepared.progressLabel } : {}) });
1728
+ payload = { result: output ?? null };
1635
1729
  }
1636
- else {
1637
- let payload;
1638
- try {
1639
- if (!this.#toolRuntime) {
1640
- throw new AgentToolError("TOOL_CONFIRMATION_INVALIDATED", `Tool "${pending.descriptor.toolName}" no longer has a local Runtime.`, { toolExecuted: false });
1641
- }
1642
- const output = await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(pending.runtimeTool, pending.input, { signal, ...(pending.prepared.progressLabel ? { progressLabel: pending.prepared.progressLabel } : {}) });
1643
- payload = { result: output ?? null };
1644
- }
1645
- catch (error) {
1646
- currentFailed = true;
1647
- const normalized = signal.aborted
1648
- ? new AgentToolError("TOOL_CANCELLED", `Tool "${pending.descriptor.toolName}" was cancelled with the Agent run.`)
1649
- : error;
1650
- payload = errorPayload(normalized);
1651
- }
1652
- const eventContent = serializeToolPayload(payload);
1730
+ catch (error) {
1731
+ currentFailed = true;
1732
+ const normalized = signal.aborted
1733
+ ? new AgentToolError("TOOL_CANCELLED", `Tool "${pending.descriptor.toolName}" was cancelled with the Agent run.`)
1734
+ : error;
1735
+ payload = errorPayload(normalized);
1736
+ }
1737
+ const eventContent = serializeToolPayload(payload);
1738
+ this.#messages.push({
1739
+ role: "tool",
1740
+ toolCallId: pending.prepared.call.id,
1741
+ name: pending.prepared.call.name,
1742
+ content: modelToolPayload(pending.prepared, payload),
1743
+ });
1744
+ emit({
1745
+ type: EventType.TOOL_CALL_RESULT,
1746
+ messageId: createId(),
1747
+ role: "tool",
1748
+ toolCallId: pending.prepared.call.id,
1749
+ content: eventContent,
1750
+ });
1751
+ for (const prepared of pending.remaining) {
1752
+ const skipped = new AgentToolError("TOOL_SKIPPED", "Skipped because an earlier tool call required confirmation.", { failedToolCallId: pending.prepared.call.id });
1753
+ const skippedPayload = errorPayload(skipped);
1653
1754
  this.#messages.push({
1654
1755
  role: "tool",
1655
- toolCallId: pending.prepared.call.id,
1656
- name: pending.prepared.call.name,
1657
- content: modelToolPayload(pending.prepared, payload),
1756
+ toolCallId: prepared.call.id,
1757
+ name: prepared.call.name,
1758
+ content: modelToolPayload(prepared, skippedPayload),
1658
1759
  });
1659
1760
  emit({
1660
1761
  type: EventType.TOOL_CALL_RESULT,
1661
1762
  messageId: createId(),
1662
1763
  role: "tool",
1663
- toolCallId: pending.prepared.call.id,
1664
- content: eventContent,
1764
+ toolCallId: prepared.call.id,
1765
+ content: serializeToolPayload(skippedPayload),
1665
1766
  });
1666
- for (const prepared of pending.remaining) {
1667
- const skipped = new AgentToolError("TOOL_SKIPPED", "Skipped because an earlier tool call required confirmation.", { failedToolCallId: pending.prepared.call.id });
1668
- const skippedPayload = errorPayload(skipped);
1669
- this.#messages.push({
1670
- role: "tool",
1671
- toolCallId: prepared.call.id,
1672
- name: prepared.call.name,
1673
- content: modelToolPayload(prepared, skippedPayload),
1674
- });
1675
- emit({
1676
- type: EventType.TOOL_CALL_RESULT,
1677
- messageId: createId(),
1678
- role: "tool",
1679
- toolCallId: prepared.call.id,
1680
- content: serializeToolPayload(skippedPayload),
1681
- });
1682
- }
1683
- }
1684
- signal.throwIfAborted();
1685
- const unresolved = new Set(pending.unresolvedToolFailures);
1686
- if (currentFailed)
1687
- unresolved.add(pending.prepared.displayName);
1688
- else
1689
- unresolved.delete(pending.prepared.displayName);
1690
- for (const prepared of pending.remaining) {
1691
- unresolved.add(prepared.displayName);
1692
1767
  }
1693
- const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
1694
- this.#appendContextCheckpoint(contexts);
1695
- const nextRecoverableFailures = pending.recoverableFailures
1696
- + (currentFailed ? 1 : 0);
1697
- if (decision === "confirm"
1698
- && currentFailed
1699
- && nextRecoverableFailures > this.#maxRetries) {
1700
- throw new AgentError("AGENT_RUN_LIMIT_REACHED", "Agent recovery attempt limit reached.");
1701
- }
1702
- const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
1703
- return this.#execute(emit, signal, pending.modelBinding, definitions, runId, {
1704
- step: pending.nextStep,
1705
- recoverableFailures: nextRecoverableFailures,
1706
- unresolvedToolFailures: unresolved,
1707
- });
1768
+ }
1769
+ signal.throwIfAborted();
1770
+ this.#loadPendingBrowserSkill(pending);
1771
+ const unresolved = new Set(pending.unresolvedToolFailures);
1772
+ if (currentFailed)
1773
+ unresolved.add(pending.prepared.displayName);
1774
+ else
1775
+ unresolved.delete(pending.prepared.displayName);
1776
+ for (const prepared of pending.remaining) {
1777
+ unresolved.add(prepared.displayName);
1778
+ }
1779
+ const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
1780
+ this.#appendContextCheckpoint(contexts);
1781
+ const nextRecoverableFailures = pending.recoverableFailures
1782
+ + (currentFailed ? 1 : 0);
1783
+ if (decision === "confirm"
1784
+ && currentFailed
1785
+ && nextRecoverableFailures > this.#maxRetries) {
1786
+ throw new AgentError("AGENT_RUN_LIMIT_REACHED", "Agent recovery attempt limit reached.");
1787
+ }
1788
+ const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
1789
+ return this.#execute(emit, signal, pending.modelBinding, definitions, runId, {
1790
+ step: pending.nextStep,
1791
+ recoverableFailures: nextRecoverableFailures,
1792
+ unresolvedToolFailures: unresolved,
1708
1793
  });
1709
- this.#activeRun = run;
1710
- void run.result.finally(() => {
1711
- if (this.#activeRun === run)
1712
- this.#activeRun = undefined;
1713
- }).catch(() => undefined);
1714
- return run;
1715
1794
  }
1716
1795
  #appendContextCheckpoint(entries) {
1717
1796
  const serializedEntries = JSON.stringify(entries);