@nexface/agent 0.1.1-alpha.5 → 0.1.1-alpha.7

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,11 +1,12 @@
1
+ import { decodeRequest, convertMessages, parseResume, approvalResponseSchema, toolResultResponseSchema, toProtocolMessages, mergeProtocolMessages, runResponse } from "./request.js";
1
2
  import { getInternalToolRuntime } from "@nexface/tools/internal";
2
3
  import { BROWSER_TOOLS_SKILL, serializeBrowserToolsSkill, } from "./browser-prompt.js";
3
4
  import { AgentError, isAgentError } from "./errors.js";
4
- import { EventType, TextInputContentSchema, ImageInputContentSchema, ResumeEntrySchema } from "@ag-ui/core";
5
+ import { EventType, RunAgentInputSchema } from "@ag-ui/core";
5
6
  import { agentBrand } from "./types.js";
6
7
  import { describeModelConfig, prepareModelConfig, resolveModelBinding, } from "./model/binding.js";
7
8
  import { AgentModelError } from "./model/error.js";
8
- import { createLocalToolBridge, isToolBridgeError, ToolBridgeError, validateBridgeContexts, validateBridgeResult, validateBridgeTools, } from "./tool-bridge.js";
9
+ import { createLocalToolBridge, validateBridgeContexts, validateBridgeTools, } from "./tool-bridge.js";
9
10
  const SYSTEM_PROMPT = "You are an application agent. Handle the latest user request.\n\n"
10
11
  + "Decision:\n"
11
12
  + "- Reply directly when the request needs no application data or action.\n"
@@ -18,7 +19,7 @@ const SYSTEM_PROMPT = "You are an application agent. Handle the latest user requ
18
19
  + "- Use only the provided function tools. Call cataloged application tools through callAppTool.\n"
19
20
  + "- If the user repeats or changes an action, execute the calls required for the latest request even if the same tool ran earlier.\n"
20
21
  + "- Before a state-changing call, use the latest context or a current read or navigation call to confirm any required state and identifiers.\n"
21
- + "- The catalog may change after navigation or opening UI. Do not batch those calls with operations that depend on newly available tools.\n"
22
+ + "- The catalog changes with the page. If a needed tool is missing and navigation to the relevant page is available, navigate there and inspect the updated catalog before declaring the task unsupported. Do not batch navigation or opening UI with operations that depend on newly available tools.\n"
22
23
  + "- Batch only independent calls. The host executes them in order and skips remaining calls after a failure.\n"
23
24
  + "- Provide a concise progressLabel in the user's language for every application or page automation call. Do not expose tool names, internal identifiers, or parameters.\n\n"
24
25
  + "Completion:\n"
@@ -341,8 +342,8 @@ function validateToolCallIds(toolCalls) {
341
342
  }
342
343
  function eventFields(prepared) {
343
344
  return {
344
- name: prepared.displayName,
345
- arguments: prepared.displayArguments,
345
+ name: prepared.call.name,
346
+ arguments: prepared.call.arguments,
346
347
  ...(prepared.displayInput === undefined ? {} : { input: prepared.displayInput }),
347
348
  ...(prepared.parseError === undefined ? {} : { parseError: prepared.parseError }),
348
349
  };
@@ -577,9 +578,6 @@ function createTextEmitter(emit) {
577
578
  emit({ type: EventType.TEXT_MESSAGE_END, messageId: id });
578
579
  } };
579
580
  }
580
- function emitText(emit, content) {
581
- createTextEmitter(emit).finish(content);
582
- }
583
581
  class AgentRunStream {
584
582
  id;
585
583
  threadId;
@@ -612,16 +610,13 @@ class AgentRunStream {
612
610
  await this.onResult?.(result, false);
613
611
  this.#controller.signal.throwIfAborted();
614
612
  this.#emit({
615
- type: EventType.RUN_FINISHED, threadId, runId: id, result,
616
- outcome: result.status === "completed" ? { type: "success" } : {
617
- type: "interrupt", interrupts: [{
618
- id: result.interrupt.id, reason: "tool_call",
619
- toolCallId: result.interrupt.toolCallId,
620
- metadata: { nexface: result.interrupt },
621
- }],
622
- },
613
+ type: EventType.RUN_FINISHED, threadId, runId: id,
614
+ outcome: result.outcome,
615
+ result: { ...(result.output === undefined ? {} : { output: result.output }) },
623
616
  });
624
- return result;
617
+ return result.outcome.type === "success"
618
+ ? { outcome: result.outcome, output: result.output }
619
+ : { outcome: result.outcome };
625
620
  }
626
621
  catch (error) {
627
622
  await this.onResult?.(undefined, this.#controller.signal.aborted);
@@ -637,6 +632,7 @@ class AgentRunStream {
637
632
  this.#notify();
638
633
  });
639
634
  }
635
+ toResponse() { return runResponse(this); }
640
636
  cancel() {
641
637
  this.onCancel?.();
642
638
  if (!this.#controller.signal.aborted) {
@@ -672,7 +668,15 @@ class Nexface {
672
668
  id = createId();
673
669
  #toolBridge;
674
670
  #toolRuntime;
675
- #externalToolBridge;
671
+ #registered = new Map();
672
+ #request;
673
+ #requestIdentity;
674
+ #requestMessages = [];
675
+ #requestPending;
676
+ #requestResult;
677
+ #requestUsed = false;
678
+ #hasRun = false;
679
+ #receiptQueue = [];
676
680
  #pendingModelConfig;
677
681
  #modelBinding;
678
682
  #modelInfo;
@@ -690,8 +694,6 @@ class Nexface {
690
694
  #browserSkillLoaded = false;
691
695
  #activeRun;
692
696
  #operation;
693
- #terminalToolBridgeError;
694
- #bridgeExecutionOccurredInRun = false;
695
697
  #pendingInterrupt;
696
698
  #authorizedPageResponses = new WeakSet();
697
699
  #hasPageConfirmation = (_toolName) => false;
@@ -703,25 +705,37 @@ class Nexface {
703
705
  : SYSTEM_PROMPT;
704
706
  const hasToolRuntime = "toolRuntime" in options
705
707
  && options.toolRuntime !== undefined;
706
- const hasToolBridge = "toolBridge" in options
707
- && options.toolBridge !== undefined;
708
- if (hasToolRuntime === hasToolBridge) {
709
- throw new AgentError("AGENT_TOOL_RUNTIME_UNAVAILABLE", "createAgent requires exactly one toolRuntime or toolBridge.");
708
+ if ("toolBridge" in options) {
709
+ throw new AgentError("AGENT_TOOL_RUNTIME_UNAVAILABLE", "toolBridge is no longer supported. Use toolRuntime locally or ChatClient with HTTP + SSE.");
710
710
  }
711
711
  if (hasToolRuntime) {
712
712
  assertToolRuntime(options.toolRuntime);
713
713
  this.#toolRuntime = options.toolRuntime;
714
714
  this.#toolBridge = createLocalToolBridge(options.toolRuntime);
715
- this.#externalToolBridge = false;
716
715
  }
717
- else if (hasToolBridge) {
718
- this.#toolBridge = options.toolBridge;
719
- this.#externalToolBridge = true;
716
+ else {
720
717
  this.#toolRuntime = undefined;
718
+ this.#toolBridge = {
719
+ getTools: async () => this.#request?.tools ?? [],
720
+ getContexts: async () => this.#request?.contexts ?? [],
721
+ execute: async () => { throw new Error("No browser transport is available."); },
722
+ };
721
723
  }
722
- else {
723
- throw new Error("Unreachable Agent Tool transport state.");
724
+ for (const tool of options.tools ?? []) {
725
+ if (!tool?.definition || typeof tool.execute !== "function" || "requiresConfirmation" in tool || this.#registered.has(tool.definition.name) || NATIVE_TOOL_NAMES.has(tool.definition.name)) {
726
+ throw new AgentError("INVALID_RUN_INPUT", "Invalid or duplicate registered Agent tool.");
727
+ }
728
+ this.#registered.set(tool.definition.name, tool);
724
729
  }
730
+ const bridge = this.#toolBridge;
731
+ this.#toolBridge = {
732
+ getTools: async (options) => validateBridgeTools([
733
+ ...await bridge.getTools(options),
734
+ ...[...this.#registered.values()].map((tool) => tool.definition),
735
+ ]),
736
+ getContexts: (options) => bridge.getContexts(options),
737
+ execute: (definition, input, options) => bridge.execute(definition, input, options),
738
+ };
725
739
  const model = prepareModelConfig(options.model);
726
740
  this.#pendingModelConfig = model;
727
741
  this.#modelInfo = describeModelConfig(model);
@@ -735,7 +749,12 @@ class Nexface {
735
749
  throw new Error("Conversation can only be restored into a new Agent.");
736
750
  this.#messages.push(...structuredClone(messages));
737
751
  },
738
- 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
+ },
739
758
  authorizePageResponse: (response) => {
740
759
  this.#authorizedPageResponses.add(response);
741
760
  },
@@ -773,134 +792,336 @@ class Nexface {
773
792
  }
774
793
  }
775
794
  run(input, options = {}) {
776
- 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)
777
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
+ }
778
880
  }
779
- if (this.#modelSwitch) {
780
- 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 };
781
884
  }
782
- if (this.#terminalToolBridgeError)
783
- throw this.#terminalToolBridgeError;
784
- if ("resume" in input) {
785
- if (!Array.isArray(input.resume) || input.resume.length !== 1) {
786
- 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;
787
904
  }
788
- const parsedEntry = ResumeEntrySchema.safeParse(input.resume[0]);
789
- if (!parsedEntry.success) {
790
- 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;
791
908
  }
792
- const entry = parsedEntry.data;
793
- if (entry.status === "cancelled") {
794
- 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);
795
915
  }
796
- if (typeof entry.payload?.approved !== "boolean") {
797
- 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
+ }
798
935
  }
799
- const pending = this.#pendingInterrupt;
800
- if (entry.payload.approved
801
- && pending?.descriptor.id === entry.interruptId
802
- && pending.descriptor.mode === "page"
803
- && !this.#authorizedPageResponses.delete(input.resume[0])) {
804
- 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
+ }
805
952
  }
806
- const continuation = this.#resolveInterrupt(entry.interruptId, entry.payload.approved ? "confirm" : "reject", options);
807
- if (!continuation) {
808
- 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);
809
956
  }
810
- return continuation;
811
- }
812
- const raw = input.message.content;
813
- const content = typeof raw === "string" ? raw.trim() : raw.map((part) => part.type === "text" ? TextInputContentSchema.parse(part) : ImageInputContentSchema.parse(part));
814
- if (!content.length) {
815
- throw new AgentError("INVALID_RUN_INPUT", "Agent message content is required.");
816
- }
817
- if (options.signal?.aborted) {
818
- return new AgentRunStream(createId(), this.id, options, async (_emit, signal) => {
819
- signal.throwIfAborted();
820
- throw new Error("An aborted Run cannot execute.");
821
- });
822
- }
823
- if (this.#toolRuntime) {
824
- const abort = new AbortController();
825
- const handle = getInternalToolRuntime(this.#toolRuntime).beginInternalOperation({
826
- signal: abort.signal,
827
- onCancel: () => {
828
- abort.abort(new DOMException("Agent operation cancelled.", "AbortError"));
829
- if (this.#operation?.abort === abort && this.#pendingInterrupt) {
830
- this.#settleInterrupt(this.#pendingInterrupt.descriptor.id, { type: "cancel" });
831
- }
832
- },
833
- ...(this.#pendingInterrupt && this.#operation ? { replaceWaiting: this.#operation.handle } : {}),
834
- });
835
- // The old handle has been replaced atomically; invalidation cannot close the new one.
836
- this.#operation = undefined;
837
- if (this.#pendingInterrupt)
838
- this.#invalidateInterrupt("A new user message started.", "new_message");
839
- this.#operation = { handle, abort };
840
- }
841
- else if (this.#pendingInterrupt) {
842
- this.#invalidateInterrupt("A new user message started.", "new_message");
843
- }
844
- const inputMessage = { role: "user", content };
845
- this.#bridgeExecutionOccurredInRun = false;
846
- const runId = createId();
847
- const pendingModelConfig = this.#pendingModelConfig;
848
- const existingBinding = this.#modelBinding;
849
- 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));
850
964
  try {
851
- const binding = existingBinding ?? await resolveModelBinding(pendingModelConfig, signal);
852
- signal.throwIfAborted();
853
- if (!this.#modelBinding && this.#pendingModelConfig === pendingModelConfig) {
854
- this.#modelBinding = binding;
855
- this.#modelInfo = binding.info;
856
- this.#pendingModelConfig = undefined;
857
- }
858
- const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
859
- this.#appendContextCheckpoint(contexts);
860
- const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
861
- const applicationDefinitions = definitions
862
- .filter(({ name }) => !NATIVE_TOOL_NAMES.has(name));
863
- appendRuntimeCatalogCheckpoint(this.#messages, applicationDefinitions);
864
- this.#messages.push(inputMessage);
865
- 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);
866
967
  }
867
968
  catch (error) {
868
- if (this.#externalToolBridge
869
- && isToolBridgeError(error)
870
- && (this.#bridgeExecutionOccurredInRun
871
- || error.code === "EXECUTION_OUTCOME_UNKNOWN")) {
872
- this.#terminalToolBridgeError = error;
873
- }
874
- // Preserve history after a Tool may have caused application side effects.
875
- if (this.#messages.at(-1) === inputMessage)
876
- 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);
877
972
  throw error;
878
973
  }
879
974
  });
880
975
  this.#activeRun = run;
881
- void run.result.then(() => {
882
- if (this.#activeRun === run)
883
- this.#activeRun = undefined;
884
- }, () => {
885
- if (this.#activeRun === run)
886
- this.#activeRun = undefined;
887
- });
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; });
888
981
  return run;
889
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
+ }
1053
+ async #executeRegistered(tool, input, toolCallId, progressLabel, emit, signal) {
1054
+ const progress = { phase: "tool", toolCallId, ...(progressLabel ? { progressLabel } : {}) };
1055
+ const runtime = this.#toolRuntime && getInternalToolRuntime(this.#toolRuntime);
1056
+ if (runtime && this.#operation)
1057
+ runtime.setTaskProgress(this.#operation.handle, progress);
1058
+ emit({ type: EventType.CUSTOM, name: "nexface.tool-execution", value: { ...progress, status: "running" } });
1059
+ try {
1060
+ return await tool.execute(input, { signal, toolCallId });
1061
+ }
1062
+ finally {
1063
+ if (runtime && this.#operation)
1064
+ runtime.setTaskProgress(this.#operation.handle, { phase: "processing" });
1065
+ emit({ type: EventType.CUSTOM, name: "nexface.tool-execution", value: { toolCallId, status: "finished" } });
1066
+ }
1067
+ }
1068
+ #exportState(binding) {
1069
+ toProtocolMessages(this.#messages);
1070
+ const messages = this.#messages.map((message) => {
1071
+ if (message.role !== "system")
1072
+ return structuredClone(message);
1073
+ if (message.content === serializeBrowserToolsSkill())
1074
+ return { role: "system", content: "nexface:browser" };
1075
+ for (const skill of this.#skills) {
1076
+ if (message.content === serializeSkill(skill))
1077
+ return { role: "system", content: `nexface:skill:${skill.name}` };
1078
+ }
1079
+ return { role: "system", content: "nexface:catalog" };
1080
+ });
1081
+ const continuation = {
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,
1083
+ contextRevision: this.#contextRevision, ...(this.#lastContextSerialized === undefined ? {} : { lastContextSerialized: this.#lastContextSerialized }),
1084
+ ...(binding.exportReplay ? { replay: binding.exportReplay(this.#messages) } : {}),
1085
+ ...(this.#requestPending ? { pending: this.#requestPending } : {}),
1086
+ };
1087
+ return structuredClone({ nexface: continuation });
1088
+ }
890
1089
  #createRun(id, options, execute) {
891
1090
  const operation = this.#operation;
892
1091
  const runOptions = operation ? {
893
1092
  ...options,
894
1093
  signal: AbortSignal.any([operation.abort.signal, ...(options.signal ? [options.signal] : [])]),
895
1094
  } : options;
896
- return new AgentRunStream(id, this.id, runOptions, async (emit, signal) => {
1095
+ return new AgentRunStream(id, this.#requestIdentity?.threadId ?? this.id, runOptions, async (emit, signal) => {
897
1096
  const onAbort = () => operation?.abort.abort(signal.reason);
898
1097
  if (signal.aborted)
899
1098
  onAbort();
900
1099
  else
901
1100
  signal.addEventListener("abort", onAbort, { once: true });
902
1101
  try {
903
- return await execute(emit, signal);
1102
+ const result = await execute((event) => {
1103
+ if (event.type === EventType.TOOL_CALL_RESULT) {
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 } : {}) };
1108
+ const message = this.#messages.at(-1);
1109
+ if (message?.role === "tool" && message.toolCallId === event.toolCallId) {
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
+ }
1116
+ }
1117
+ emit(event);
1118
+ }, signal);
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) });
1124
+ return result;
904
1125
  }
905
1126
  finally {
906
1127
  signal.removeEventListener("abort", onAbort);
@@ -915,7 +1136,7 @@ class Nexface {
915
1136
  }, async (result, aborted) => {
916
1137
  if (!operation || this.#operation !== operation)
917
1138
  return;
918
- if (result?.status === "interrupted" && this.#pendingInterrupt) {
1139
+ if (result?.outcome.type === "interrupt" && this.#pendingInterrupt) {
919
1140
  operation.handle.setWaitingForUser(true);
920
1141
  }
921
1142
  else {
@@ -958,13 +1179,13 @@ class Nexface {
958
1179
  throw new AgentError("AGENT_RUN_LIMIT_REACHED", "Agent step limit reached before completion.");
959
1180
  }
960
1181
  this.#messages.push({
961
- role: "assistant",
1182
+ id: text.id, role: "assistant",
962
1183
  content: response.content,
963
1184
  toolCalls: [],
964
1185
  });
965
1186
  response.accept?.();
966
1187
  text.finish(response.content);
967
- return { status: "completed", output };
1188
+ return { outcome: { type: "success" }, output };
968
1189
  }
969
1190
  for (let step = startStep; step < this.#maxSteps; step += 1) {
970
1191
  signal.throwIfAborted();
@@ -992,14 +1213,16 @@ class Nexface {
992
1213
  finishTool,
993
1214
  ];
994
1215
  let response;
995
- const browserSkillAvailableForRequest = this.#browserSkillLoaded;
1216
+ const resumed = this.#requestPending;
1217
+ this.#requestPending = undefined;
1218
+ const browserSkillAvailableForRequest = resumed?.browserSkillAvailable ?? this.#browserSkillLoaded;
996
1219
  appendRuntimeCatalogCheckpoint(this.#messages, applicationDefinitions);
997
1220
  const messages = [
998
1221
  { role: "system", content: this.#systemPrompt },
999
1222
  ...this.#messages,
1000
1223
  ];
1001
1224
  const text = createTextEmitter(emit);
1002
- response = await binding.complete({
1225
+ response = resumed ? { content: null, toolCalls: resumed.calls } : await binding.complete({
1003
1226
  onTextDelta: text.delta,
1004
1227
  messages,
1005
1228
  tools,
@@ -1023,8 +1246,10 @@ class Nexface {
1023
1246
  if (!output) {
1024
1247
  throw new AgentError("INVALID_MODEL_RESPONSE", "finish needs non-empty output.");
1025
1248
  }
1249
+ if (!response.content)
1250
+ text.finish(output);
1026
1251
  this.#messages.push({
1027
- role: "assistant",
1252
+ id: text.id, role: "assistant",
1028
1253
  content: output,
1029
1254
  toolCalls: response.toolCalls,
1030
1255
  });
@@ -1037,25 +1262,20 @@ class Nexface {
1037
1262
  result: { status: finishInput.status, output },
1038
1263
  }),
1039
1264
  });
1040
- if (output !== response.content?.trim())
1041
- emitText(emit, output);
1042
- return { status: "completed", output };
1265
+ return { outcome: { type: "success" }, output };
1043
1266
  }
1044
1267
  const call = response.toolCalls[0];
1045
1268
  const output = response.content?.trim() ?? "";
1046
1269
  if (!call && !output) {
1047
1270
  throw new AgentError("INVALID_MODEL_RESPONSE", "Model returned neither a message nor a tool call.");
1048
1271
  }
1049
- const batchHistoryStart = this.#messages.length;
1050
- this.#messages.push({
1051
- role: "assistant",
1052
- content: response.content,
1053
- toolCalls: response.toolCalls,
1054
- });
1055
- response.accept?.();
1272
+ if (!resumed) {
1273
+ this.#messages.push({ id: text.id, role: "assistant", content: response.content, toolCalls: response.toolCalls });
1274
+ response.accept?.();
1275
+ }
1056
1276
  const preparedCalls = response.toolCalls.map((modelCall) => prepareToolCall(modelCall, nativeToolNames, this.#skills.length > 0));
1057
1277
  const publicCalls = preparedCalls.filter(({ kind }) => kind !== "finish");
1058
- for (const prepared of publicCalls) {
1278
+ for (const prepared of resumed ? [] : publicCalls) {
1059
1279
  const fields = eventFields(prepared);
1060
1280
  emit({ type: EventType.TOOL_CALL_START, toolCallId: prepared.call.id,
1061
1281
  toolCallName: fields.name, parentMessageId: text.id });
@@ -1066,7 +1286,7 @@ class Nexface {
1066
1286
  }
1067
1287
  if (!call) {
1068
1288
  return {
1069
- status: "completed",
1289
+ outcome: { type: "success" },
1070
1290
  output,
1071
1291
  };
1072
1292
  }
@@ -1127,11 +1347,12 @@ class Nexface {
1127
1347
  continue;
1128
1348
  }
1129
1349
  let failedCallId;
1130
- let shouldLoadBrowserSkill = false;
1350
+ let shouldLoadBrowserSkill = resumed?.loadBrowserSkill ?? false;
1351
+ let confirmedCallId;
1131
1352
  let skillToLoad;
1132
1353
  for (let index = 0; index < preparedCalls.length; index += 1) {
1133
1354
  const prepared = preparedCalls[index];
1134
- if (failedCallId) {
1355
+ if (failedCallId || confirmedCallId) {
1135
1356
  const skipped = new AgentToolError("TOOL_SKIPPED", "Skipped because an earlier tool call failed.", { failedToolCallId: failedCallId });
1136
1357
  const payload = errorPayload(skipped);
1137
1358
  this.#messages.push({
@@ -1154,12 +1375,22 @@ class Nexface {
1154
1375
  let modelContent;
1155
1376
  let eventContent;
1156
1377
  let failed = false;
1157
- let bridgeExecutionConfirmed = false;
1158
1378
  try {
1159
1379
  if (prepared.preparationError)
1160
1380
  throw prepared.preparationError;
1161
1381
  let output;
1162
- if (prepared.kind === "skill") {
1382
+ const browserResult = this.#requestResult?.toolCallId === prepared.call.id ? this.#requestResult : undefined;
1383
+ if (browserResult) {
1384
+ this.#requestResult = undefined;
1385
+ if (browserResult.confirmed)
1386
+ confirmedCallId = prepared.call.id;
1387
+ if (browserResult.error)
1388
+ throw new AgentToolError(browserResult.error.code, browserResult.error.message);
1389
+ output = browserResult.result;
1390
+ if (prepared.call.name === "getSnapshot")
1391
+ shouldLoadBrowserSkill = true;
1392
+ }
1393
+ else if (prepared.kind === "skill") {
1163
1394
  toolInput = parseArguments(prepared.call);
1164
1395
  const unexpectedKeys = Object.keys(toolInput)
1165
1396
  .filter((key) => key !== "name");
@@ -1184,8 +1415,9 @@ class Nexface {
1184
1415
  if (!expectedDefinition) {
1185
1416
  throw new AgentToolError("UNKNOWN_TOOL", `Tool "${prepared.displayName}" was not available when this model request was sent.`);
1186
1417
  }
1418
+ const registered = this.#registered.get(prepared.displayName);
1187
1419
  const runtimeTool = this.#toolRuntime?.getTools().find((candidate) => candidate.definition === expectedDefinition);
1188
- if (this.#toolRuntime && !runtimeTool) {
1420
+ if (this.#toolRuntime && !runtimeTool && !registered) {
1189
1421
  const replacement = this.#toolRuntime.getTools().find(({ definition }) => definition.name === expectedDefinition.name);
1190
1422
  throw new AgentToolError(replacement ? "CONTRACT_MISMATCH" : "UNKNOWN_TOOL", replacement
1191
1423
  ? `Tool "${expectedDefinition.name}" changed after the model request was sent.`
@@ -1195,6 +1427,10 @@ class Nexface {
1195
1427
  if (inputErrors.length) {
1196
1428
  throw new AgentToolError("INVALID_INPUT", inputErrors.join(" "), { errors: inputErrors });
1197
1429
  }
1430
+ if (this.#request && !registered) {
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 } : {}) } } } }] } };
1433
+ }
1198
1434
  const confirmation = runtimeTool?.confirmation;
1199
1435
  if (runtimeTool && confirmation) {
1200
1436
  if (confirmation.mode === "page"
@@ -1232,6 +1468,7 @@ class Nexface {
1232
1468
  remaining: preparedCalls.slice(index + 1),
1233
1469
  modelBinding: binding,
1234
1470
  nextStep: step + 1,
1471
+ loadBrowserSkill: shouldLoadBrowserSkill,
1235
1472
  recoverableFailures,
1236
1473
  unresolvedToolFailures: new Set(unresolvedToolFailures),
1237
1474
  removeInvalidationListener: () => { },
@@ -1244,15 +1481,11 @@ class Nexface {
1244
1481
  runtimeTool.lifecycleSignal.addEventListener("abort", onRuntimeToolDisposed, { once: true });
1245
1482
  pending.removeInvalidationListener = () => runtimeTool.lifecycleSignal.removeEventListener("abort", onRuntimeToolDisposed);
1246
1483
  this.#pendingInterrupt = pending;
1247
- return { status: "interrupted", interrupt: 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 } } }] } };
1248
1485
  }
1249
- output = runtimeTool && this.#toolRuntime
1486
+ output = registered ? await this.#executeRegistered(registered, toolInput, prepared.call.id, prepared.progressLabel, emit, signal) : runtimeTool && this.#toolRuntime
1250
1487
  ? await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(runtimeTool, toolInput, { signal, ...(prepared.progressLabel ? { progressLabel: prepared.progressLabel } : {}) })
1251
1488
  : await this.#toolBridge.execute(expectedDefinition, toolInput, { signal });
1252
- if (this.#externalToolBridge) {
1253
- bridgeExecutionConfirmed = true;
1254
- this.#bridgeExecutionOccurredInRun = true;
1255
- }
1256
1489
  }
1257
1490
  else if (prepared.kind === "native") {
1258
1491
  const expectedDefinition = nativeDefinitionsByName.get(prepared.call.name);
@@ -1270,14 +1503,17 @@ class Nexface {
1270
1503
  toolInput = parseArguments(prepared.call);
1271
1504
  const progressLabel = normalizeProgressLabel(toolInput.progressLabel);
1272
1505
  delete toolInput.progressLabel;
1506
+ if (this.#request) {
1507
+ const inputErrors = validateToolInput(expectedDefinition.inputSchema, toolInput);
1508
+ if (inputErrors.length)
1509
+ throw new AgentToolError("INVALID_INPUT", inputErrors.join(" "));
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 } : {}) } } } }] } };
1512
+ }
1273
1513
  const nativeRuntimeTool = this.#toolRuntime?.getTools().find((tool) => tool.definition === expectedDefinition);
1274
1514
  output = nativeRuntimeTool && this.#toolRuntime
1275
1515
  ? await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(nativeRuntimeTool, toolInput, { signal, ...(progressLabel ? { progressLabel } : {}) })
1276
1516
  : await this.#toolBridge.execute(expectedDefinition, toolInput, { signal });
1277
- if (this.#externalToolBridge) {
1278
- bridgeExecutionConfirmed = true;
1279
- this.#bridgeExecutionOccurredInRun = true;
1280
- }
1281
1517
  if (prepared.call.name === "getSnapshot") {
1282
1518
  shouldLoadBrowserSkill = true;
1283
1519
  }
@@ -1285,27 +1521,12 @@ class Nexface {
1285
1521
  else {
1286
1522
  throw new AgentError("UNKNOWN_MODEL_TOOL", `Unknown model tool: ${prepared.call.name}.`);
1287
1523
  }
1288
- if (this.#externalToolBridge)
1289
- validateBridgeResult(output);
1290
1524
  const payload = { result: output ?? null };
1291
1525
  modelContent = modelToolPayload(prepared, payload);
1292
1526
  eventContent = serializeToolPayload(payload);
1293
1527
  }
1294
1528
  catch (error) {
1295
- if (this.#externalToolBridge
1296
- && isToolBridgeError(error)
1297
- && error.code === "EXECUTION_OUTCOME_UNKNOWN") {
1298
- this.#bridgeExecutionOccurredInRun = true;
1299
- this.#messages.splice(batchHistoryStart);
1300
- throw error;
1301
- }
1302
1529
  if (signal.aborted) {
1303
- if (this.#externalToolBridge) {
1304
- this.#terminalToolBridgeError = new ToolBridgeError("EXECUTION_OUTCOME_UNKNOWN", `Tool "${prepared.displayName}" was cancelled before its outcome was confirmed.`, {
1305
- toolCallId: prepared.call.id,
1306
- toolName: prepared.displayName,
1307
- });
1308
- }
1309
1530
  const cancelled = new AgentToolError("TOOL_CANCELLED", `Tool "${prepared.displayName}" was cancelled with the Agent run.`, prepared.call.name === "executeCode"
1310
1531
  ? { refsExpired: true }
1311
1532
  : undefined);
@@ -1342,14 +1563,6 @@ class Nexface {
1342
1563
  }
1343
1564
  throw signal.reason;
1344
1565
  }
1345
- if (this.#externalToolBridge && isToolBridgeError(error)) {
1346
- this.#messages.splice(batchHistoryStart);
1347
- if (bridgeExecutionConfirmed
1348
- || error.code === "EXECUTION_OUTCOME_UNKNOWN") {
1349
- this.#bridgeExecutionOccurredInRun = true;
1350
- }
1351
- throw error;
1352
- }
1353
1566
  failed = true;
1354
1567
  failedCallId = prepared.call.id;
1355
1568
  const payload = errorPayload(error);
@@ -1444,6 +1657,37 @@ class Nexface {
1444
1657
  });
1445
1658
  }
1446
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
+ }
1447
1691
  }
1448
1692
  #invalidateInterrupt(message, reason) {
1449
1693
  const pending = this.#pendingInterrupt;
@@ -1468,103 +1712,85 @@ class Nexface {
1468
1712
  this.#appendInterruptedBatch(pending, errorPayload(new AgentToolError("TOOL_CONFIRMATION_INVALIDATED", settlement.message, { reason: settlement.reason, toolExecuted: false })));
1469
1713
  return true;
1470
1714
  }
1471
- #resolveInterrupt(id, decision, options = {}) {
1472
- const current = this.#pendingInterrupt;
1473
- if (!current || current.descriptor.id !== id)
1474
- return undefined;
1475
- if (decision === "reject" && current.descriptor.mode !== "chat") {
1476
- return undefined;
1477
- }
1478
- options.signal?.throwIfAborted();
1715
+ async #continueApproval(id, decision, emit, signal, runId) {
1479
1716
  const pending = this.#takeInterrupt(id);
1480
1717
  if (!pending)
1481
- return undefined;
1718
+ throw new AgentError("INVALID_RUN_INPUT", "The interrupt is no longer pending.");
1482
1719
  this.#operation?.handle.setWaitingForUser(false);
1483
- const runId = createId();
1484
- const run = this.#createRun(runId, options, async (emit, signal) => {
1485
- let currentFailed = decision === "reject";
1486
- if (decision === "reject") {
1487
- 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 };
1488
1729
  }
1489
- else {
1490
- let payload;
1491
- try {
1492
- if (!this.#toolRuntime) {
1493
- throw new AgentToolError("TOOL_CONFIRMATION_INVALIDATED", `Tool "${pending.descriptor.toolName}" no longer has a local Runtime.`, { toolExecuted: false });
1494
- }
1495
- const output = await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(pending.runtimeTool, pending.input, { signal, ...(pending.prepared.progressLabel ? { progressLabel: pending.prepared.progressLabel } : {}) });
1496
- payload = { result: output ?? null };
1497
- }
1498
- catch (error) {
1499
- currentFailed = true;
1500
- const normalized = signal.aborted
1501
- ? new AgentToolError("TOOL_CANCELLED", `Tool "${pending.descriptor.toolName}" was cancelled with the Agent run.`)
1502
- : error;
1503
- payload = errorPayload(normalized);
1504
- }
1505
- 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);
1506
1754
  this.#messages.push({
1507
1755
  role: "tool",
1508
- toolCallId: pending.prepared.call.id,
1509
- name: pending.prepared.call.name,
1510
- content: modelToolPayload(pending.prepared, payload),
1756
+ toolCallId: prepared.call.id,
1757
+ name: prepared.call.name,
1758
+ content: modelToolPayload(prepared, skippedPayload),
1511
1759
  });
1512
1760
  emit({
1513
1761
  type: EventType.TOOL_CALL_RESULT,
1514
1762
  messageId: createId(),
1515
1763
  role: "tool",
1516
- toolCallId: pending.prepared.call.id,
1517
- content: eventContent,
1764
+ toolCallId: prepared.call.id,
1765
+ content: serializeToolPayload(skippedPayload),
1518
1766
  });
1519
- for (const prepared of pending.remaining) {
1520
- const skipped = new AgentToolError("TOOL_SKIPPED", "Skipped because an earlier tool call required confirmation.", { failedToolCallId: pending.prepared.call.id });
1521
- const skippedPayload = errorPayload(skipped);
1522
- this.#messages.push({
1523
- role: "tool",
1524
- toolCallId: prepared.call.id,
1525
- name: prepared.call.name,
1526
- content: modelToolPayload(prepared, skippedPayload),
1527
- });
1528
- emit({
1529
- type: EventType.TOOL_CALL_RESULT,
1530
- messageId: createId(),
1531
- role: "tool",
1532
- toolCallId: prepared.call.id,
1533
- content: serializeToolPayload(skippedPayload),
1534
- });
1535
- }
1536
- }
1537
- signal.throwIfAborted();
1538
- const unresolved = new Set(pending.unresolvedToolFailures);
1539
- if (currentFailed)
1540
- unresolved.add(pending.prepared.displayName);
1541
- else
1542
- unresolved.delete(pending.prepared.displayName);
1543
- for (const prepared of pending.remaining) {
1544
- unresolved.add(prepared.displayName);
1545
- }
1546
- const contexts = validateBridgeContexts(await this.#toolBridge.getContexts({ signal }));
1547
- this.#appendContextCheckpoint(contexts);
1548
- const nextRecoverableFailures = pending.recoverableFailures
1549
- + (currentFailed ? 1 : 0);
1550
- if (decision === "confirm"
1551
- && currentFailed
1552
- && nextRecoverableFailures > this.#maxRetries) {
1553
- throw new AgentError("AGENT_RUN_LIMIT_REACHED", "Agent recovery attempt limit reached.");
1554
1767
  }
1555
- const definitions = validateBridgeTools(await this.#toolBridge.getTools({ signal }));
1556
- return this.#execute(emit, signal, pending.modelBinding, definitions, runId, {
1557
- step: pending.nextStep,
1558
- recoverableFailures: nextRecoverableFailures,
1559
- unresolvedToolFailures: unresolved,
1560
- });
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,
1561
1793
  });
1562
- this.#activeRun = run;
1563
- void run.result.finally(() => {
1564
- if (this.#activeRun === run)
1565
- this.#activeRun = undefined;
1566
- }).catch(() => undefined);
1567
- return run;
1568
1794
  }
1569
1795
  #appendContextCheckpoint(entries) {
1570
1796
  const serializedEntries = JSON.stringify(entries);