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

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.d.ts CHANGED
@@ -1,19 +1,2 @@
1
- import type { ToolRuntime } from "@nexface/tools";
2
- import type { Agent, AgentSkill } from "./types.js";
3
- import type { AgentModelConfig } from "./model/types.js";
4
- import { type ToolBridge } from "./tool-bridge.js";
5
- export declare function createAgent(options: {
6
- model: AgentModelConfig;
7
- skills?: readonly AgentSkill[];
8
- maxSteps?: number;
9
- maxRetries?: number;
10
- applicationContext?: {
11
- maxSizeBytes?: number;
12
- };
13
- } & ({
14
- toolRuntime: ToolRuntime;
15
- toolBridge?: never;
16
- } | {
17
- toolBridge: ToolBridge;
18
- toolRuntime?: never;
19
- })): Agent;
1
+ import type { Agent, CreateAgentOptions } from "./types.js";
2
+ export declare function createAgent(options: CreateAgentOptions): Agent;
package/dist/agent.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { decodeRequest, 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";
@@ -5,7 +6,7 @@ import { EventType, TextInputContentSchema, ImageInputContentSchema, ResumeEntry
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,14 +610,9 @@ 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 }), ...(result.checkpoint ? { checkpoint: result.checkpoint } : {}) },
623
616
  });
624
617
  return result;
625
618
  }
@@ -637,6 +630,7 @@ class AgentRunStream {
637
630
  this.#notify();
638
631
  });
639
632
  }
633
+ toResponse() { return runResponse(this); }
640
634
  cancel() {
641
635
  this.onCancel?.();
642
636
  if (!this.#controller.signal.aborted) {
@@ -672,7 +666,13 @@ class Nexface {
672
666
  id = createId();
673
667
  #toolBridge;
674
668
  #toolRuntime;
675
- #externalToolBridge;
669
+ #registered = new Map();
670
+ #request;
671
+ #requestIdentity;
672
+ #requestMessages = [];
673
+ #requestPending;
674
+ #requestResult;
675
+ #requestUsed = false;
676
676
  #pendingModelConfig;
677
677
  #modelBinding;
678
678
  #modelInfo;
@@ -690,8 +690,6 @@ class Nexface {
690
690
  #browserSkillLoaded = false;
691
691
  #activeRun;
692
692
  #operation;
693
- #terminalToolBridgeError;
694
- #bridgeExecutionOccurredInRun = false;
695
693
  #pendingInterrupt;
696
694
  #authorizedPageResponses = new WeakSet();
697
695
  #hasPageConfirmation = (_toolName) => false;
@@ -703,25 +701,37 @@ class Nexface {
703
701
  : SYSTEM_PROMPT;
704
702
  const hasToolRuntime = "toolRuntime" in options
705
703
  && 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.");
704
+ 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.");
710
706
  }
711
707
  if (hasToolRuntime) {
712
708
  assertToolRuntime(options.toolRuntime);
713
709
  this.#toolRuntime = options.toolRuntime;
714
710
  this.#toolBridge = createLocalToolBridge(options.toolRuntime);
715
- this.#externalToolBridge = false;
716
711
  }
717
- else if (hasToolBridge) {
718
- this.#toolBridge = options.toolBridge;
719
- this.#externalToolBridge = true;
712
+ else {
720
713
  this.#toolRuntime = undefined;
714
+ this.#toolBridge = {
715
+ getTools: async () => this.#request?.tools ?? [],
716
+ getContexts: async () => this.#request?.contexts ?? [],
717
+ execute: async () => { throw new Error("No browser transport is available."); },
718
+ };
721
719
  }
722
- else {
723
- throw new Error("Unreachable Agent Tool transport state.");
720
+ for (const tool of options.tools ?? []) {
721
+ if (!tool?.definition || typeof tool.execute !== "function" || "requiresConfirmation" in tool || this.#registered.has(tool.definition.name) || NATIVE_TOOL_NAMES.has(tool.definition.name)) {
722
+ throw new AgentError("INVALID_RUN_INPUT", "Invalid or duplicate registered Agent tool.");
723
+ }
724
+ this.#registered.set(tool.definition.name, tool);
724
725
  }
726
+ const bridge = this.#toolBridge;
727
+ this.#toolBridge = {
728
+ getTools: async (options) => validateBridgeTools([
729
+ ...await bridge.getTools(options),
730
+ ...[...this.#registered.values()].map((tool) => tool.definition),
731
+ ]),
732
+ getContexts: (options) => bridge.getContexts(options),
733
+ execute: (definition, input, options) => bridge.execute(definition, input, options),
734
+ };
725
735
  const model = prepareModelConfig(options.model);
726
736
  this.#pendingModelConfig = model;
727
737
  this.#modelInfo = describeModelConfig(model);
@@ -773,14 +783,14 @@ class Nexface {
773
783
  }
774
784
  }
775
785
  run(input, options = {}) {
786
+ if ("messages" in input)
787
+ return this.#runRequest(input, options);
776
788
  if (this.#activeRun) {
777
789
  throw new AgentError("AGENT_RUN_ACTIVE", "An agent run is already active.");
778
790
  }
779
791
  if (this.#modelSwitch) {
780
792
  throw new AgentModelError("AGENT_RUN_BLOCKED", "An agent run cannot start while the model is switching.");
781
793
  }
782
- if (this.#terminalToolBridgeError)
783
- throw this.#terminalToolBridgeError;
784
794
  if ("resume" in input) {
785
795
  if (!Array.isArray(input.resume) || input.resume.length !== 1) {
786
796
  throw new AgentError("INVALID_RUN_INPUT", "Nexface currently requires exactly one resume entry.");
@@ -824,6 +834,7 @@ class Nexface {
824
834
  const abort = new AbortController();
825
835
  const handle = getInternalToolRuntime(this.#toolRuntime).beginInternalOperation({
826
836
  signal: abort.signal,
837
+ taskProgress: { phase: "processing" },
827
838
  onCancel: () => {
828
839
  abort.abort(new DOMException("Agent operation cancelled.", "AbortError"));
829
840
  if (this.#operation?.abort === abort && this.#pendingInterrupt) {
@@ -841,8 +852,7 @@ class Nexface {
841
852
  else if (this.#pendingInterrupt) {
842
853
  this.#invalidateInterrupt("A new user message started.", "new_message");
843
854
  }
844
- const inputMessage = { role: "user", content };
845
- this.#bridgeExecutionOccurredInRun = false;
855
+ const inputMessage = { id: input.message.id ?? createId(), role: "user", content };
846
856
  const runId = createId();
847
857
  const pendingModelConfig = this.#pendingModelConfig;
848
858
  const existingBinding = this.#modelBinding;
@@ -865,12 +875,6 @@ class Nexface {
865
875
  return await this.#execute(emit, signal, binding, definitions, runId);
866
876
  }
867
877
  catch (error) {
868
- if (this.#externalToolBridge
869
- && isToolBridgeError(error)
870
- && (this.#bridgeExecutionOccurredInRun
871
- || error.code === "EXECUTION_OUTCOME_UNKNOWN")) {
872
- this.#terminalToolBridgeError = error;
873
- }
874
878
  // Preserve history after a Tool may have caused application side effects.
875
879
  if (this.#messages.at(-1) === inputMessage)
876
880
  this.#messages.pop();
@@ -887,20 +891,172 @@ class Nexface {
887
891
  });
888
892
  return run;
889
893
  }
894
+ async #executeRegistered(tool, input, toolCallId, progressLabel, emit, signal) {
895
+ const progress = { phase: "tool", toolCallId, ...(progressLabel ? { progressLabel } : {}) };
896
+ const runtime = this.#toolRuntime && getInternalToolRuntime(this.#toolRuntime);
897
+ if (runtime && this.#operation)
898
+ runtime.setTaskProgress(this.#operation.handle, progress);
899
+ emit({ type: EventType.CUSTOM, name: "nexface.tool-execution", value: { ...progress, status: "running" } });
900
+ try {
901
+ return await tool.execute(input, { signal, toolCallId });
902
+ }
903
+ finally {
904
+ if (runtime && this.#operation)
905
+ runtime.setTaskProgress(this.#operation.handle, { phase: "processing" });
906
+ emit({ type: EventType.CUSTOM, name: "nexface.tool-execution", value: { toolCallId, status: "finished" } });
907
+ }
908
+ }
909
+ #checkpoint(binding) {
910
+ toProtocolMessages(this.#messages);
911
+ const messages = this.#messages.map((message) => {
912
+ if (message.role !== "system")
913
+ return structuredClone(message);
914
+ if (message.content === serializeBrowserToolsSkill())
915
+ return { role: "system", content: "nexface:browser" };
916
+ for (const skill of this.#skills) {
917
+ if (message.content === serializeSkill(skill))
918
+ return { role: "system", content: `nexface:skill:${skill.name}` };
919
+ }
920
+ return { role: "system", content: "nexface:catalog" };
921
+ });
922
+ 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,
924
+ contextRevision: this.#contextRevision, ...(this.#lastContextSerialized === undefined ? {} : { lastContextSerialized: this.#lastContextSerialized }),
925
+ ...(binding.exportReplay ? { replay: binding.exportReplay(this.#messages) } : {}),
926
+ ...(this.#requestPending ? { pending: this.#requestPending } : {}),
927
+ };
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;
1030
+ }
890
1031
  #createRun(id, options, execute) {
891
1032
  const operation = this.#operation;
892
1033
  const runOptions = operation ? {
893
1034
  ...options,
894
1035
  signal: AbortSignal.any([operation.abort.signal, ...(options.signal ? [options.signal] : [])]),
895
1036
  } : options;
896
- return new AgentRunStream(id, this.id, runOptions, async (emit, signal) => {
1037
+ let resumedResultId = this.#request?.toolResultMessageId;
1038
+ return new AgentRunStream(id, this.#requestIdentity?.threadId ?? this.id, runOptions, async (emit, signal) => {
897
1039
  const onAbort = () => operation?.abort.abort(signal.reason);
898
1040
  if (signal.aborted)
899
1041
  onAbort();
900
1042
  else
901
1043
  signal.addEventListener("abort", onAbort, { once: true });
902
1044
  try {
903
- return await execute(emit, signal);
1045
+ const result = await execute((event) => {
1046
+ 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
+ }
1052
+ const message = this.#messages.at(-1);
1053
+ if (message?.role === "tool" && message.toolCallId === event.toolCallId)
1054
+ message.id = event.messageId;
1055
+ }
1056
+ emit(event);
1057
+ }, signal);
1058
+ emit({ type: EventType.MESSAGES_SNAPSHOT, messages: mergeProtocolMessages(toProtocolMessages(this.#messages), this.#requestMessages) });
1059
+ return result;
904
1060
  }
905
1061
  finally {
906
1062
  signal.removeEventListener("abort", onAbort);
@@ -915,7 +1071,7 @@ class Nexface {
915
1071
  }, async (result, aborted) => {
916
1072
  if (!operation || this.#operation !== operation)
917
1073
  return;
918
- if (result?.status === "interrupted" && this.#pendingInterrupt) {
1074
+ if (result?.outcome.type === "interrupt" && this.#pendingInterrupt) {
919
1075
  operation.handle.setWaitingForUser(true);
920
1076
  }
921
1077
  else {
@@ -958,13 +1114,13 @@ class Nexface {
958
1114
  throw new AgentError("AGENT_RUN_LIMIT_REACHED", "Agent step limit reached before completion.");
959
1115
  }
960
1116
  this.#messages.push({
961
- role: "assistant",
1117
+ id: text.id, role: "assistant",
962
1118
  content: response.content,
963
1119
  toolCalls: [],
964
1120
  });
965
1121
  response.accept?.();
966
1122
  text.finish(response.content);
967
- return { status: "completed", output };
1123
+ return { outcome: { type: "success" }, output };
968
1124
  }
969
1125
  for (let step = startStep; step < this.#maxSteps; step += 1) {
970
1126
  signal.throwIfAborted();
@@ -992,14 +1148,16 @@ class Nexface {
992
1148
  finishTool,
993
1149
  ];
994
1150
  let response;
995
- const browserSkillAvailableForRequest = this.#browserSkillLoaded;
1151
+ const resumed = this.#requestPending;
1152
+ this.#requestPending = undefined;
1153
+ const browserSkillAvailableForRequest = resumed?.browserSkillAvailable ?? this.#browserSkillLoaded;
996
1154
  appendRuntimeCatalogCheckpoint(this.#messages, applicationDefinitions);
997
1155
  const messages = [
998
1156
  { role: "system", content: this.#systemPrompt },
999
1157
  ...this.#messages,
1000
1158
  ];
1001
1159
  const text = createTextEmitter(emit);
1002
- response = await binding.complete({
1160
+ response = resumed ? { content: null, toolCalls: resumed.calls } : await binding.complete({
1003
1161
  onTextDelta: text.delta,
1004
1162
  messages,
1005
1163
  tools,
@@ -1023,8 +1181,10 @@ class Nexface {
1023
1181
  if (!output) {
1024
1182
  throw new AgentError("INVALID_MODEL_RESPONSE", "finish needs non-empty output.");
1025
1183
  }
1184
+ if (!response.content)
1185
+ text.finish(output);
1026
1186
  this.#messages.push({
1027
- role: "assistant",
1187
+ id: text.id, role: "assistant",
1028
1188
  content: output,
1029
1189
  toolCalls: response.toolCalls,
1030
1190
  });
@@ -1037,25 +1197,20 @@ class Nexface {
1037
1197
  result: { status: finishInput.status, output },
1038
1198
  }),
1039
1199
  });
1040
- if (output !== response.content?.trim())
1041
- emitText(emit, output);
1042
- return { status: "completed", output };
1200
+ return { outcome: { type: "success" }, output };
1043
1201
  }
1044
1202
  const call = response.toolCalls[0];
1045
1203
  const output = response.content?.trim() ?? "";
1046
1204
  if (!call && !output) {
1047
1205
  throw new AgentError("INVALID_MODEL_RESPONSE", "Model returned neither a message nor a tool call.");
1048
1206
  }
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?.();
1207
+ if (!resumed) {
1208
+ this.#messages.push({ id: text.id, role: "assistant", content: response.content, toolCalls: response.toolCalls });
1209
+ response.accept?.();
1210
+ }
1056
1211
  const preparedCalls = response.toolCalls.map((modelCall) => prepareToolCall(modelCall, nativeToolNames, this.#skills.length > 0));
1057
1212
  const publicCalls = preparedCalls.filter(({ kind }) => kind !== "finish");
1058
- for (const prepared of publicCalls) {
1213
+ for (const prepared of resumed ? [] : publicCalls) {
1059
1214
  const fields = eventFields(prepared);
1060
1215
  emit({ type: EventType.TOOL_CALL_START, toolCallId: prepared.call.id,
1061
1216
  toolCallName: fields.name, parentMessageId: text.id });
@@ -1066,7 +1221,7 @@ class Nexface {
1066
1221
  }
1067
1222
  if (!call) {
1068
1223
  return {
1069
- status: "completed",
1224
+ outcome: { type: "success" },
1070
1225
  output,
1071
1226
  };
1072
1227
  }
@@ -1127,11 +1282,12 @@ class Nexface {
1127
1282
  continue;
1128
1283
  }
1129
1284
  let failedCallId;
1130
- let shouldLoadBrowserSkill = false;
1285
+ let shouldLoadBrowserSkill = resumed?.loadBrowserSkill ?? false;
1286
+ let confirmedCallId;
1131
1287
  let skillToLoad;
1132
1288
  for (let index = 0; index < preparedCalls.length; index += 1) {
1133
1289
  const prepared = preparedCalls[index];
1134
- if (failedCallId) {
1290
+ if (failedCallId || confirmedCallId) {
1135
1291
  const skipped = new AgentToolError("TOOL_SKIPPED", "Skipped because an earlier tool call failed.", { failedToolCallId: failedCallId });
1136
1292
  const payload = errorPayload(skipped);
1137
1293
  this.#messages.push({
@@ -1154,12 +1310,22 @@ class Nexface {
1154
1310
  let modelContent;
1155
1311
  let eventContent;
1156
1312
  let failed = false;
1157
- let bridgeExecutionConfirmed = false;
1158
1313
  try {
1159
1314
  if (prepared.preparationError)
1160
1315
  throw prepared.preparationError;
1161
1316
  let output;
1162
- if (prepared.kind === "skill") {
1317
+ const browserResult = this.#requestResult?.toolCallId === prepared.call.id ? this.#requestResult : undefined;
1318
+ if (browserResult) {
1319
+ this.#requestResult = undefined;
1320
+ if (browserResult.confirmed)
1321
+ confirmedCallId = prepared.call.id;
1322
+ if (browserResult.error)
1323
+ throw new AgentToolError(browserResult.error.code, browserResult.error.message);
1324
+ output = browserResult.result;
1325
+ if (prepared.call.name === "getSnapshot")
1326
+ shouldLoadBrowserSkill = true;
1327
+ }
1328
+ else if (prepared.kind === "skill") {
1163
1329
  toolInput = parseArguments(prepared.call);
1164
1330
  const unexpectedKeys = Object.keys(toolInput)
1165
1331
  .filter((key) => key !== "name");
@@ -1184,8 +1350,9 @@ class Nexface {
1184
1350
  if (!expectedDefinition) {
1185
1351
  throw new AgentToolError("UNKNOWN_TOOL", `Tool "${prepared.displayName}" was not available when this model request was sent.`);
1186
1352
  }
1353
+ const registered = this.#registered.get(prepared.displayName);
1187
1354
  const runtimeTool = this.#toolRuntime?.getTools().find((candidate) => candidate.definition === expectedDefinition);
1188
- if (this.#toolRuntime && !runtimeTool) {
1355
+ if (this.#toolRuntime && !runtimeTool && !registered) {
1189
1356
  const replacement = this.#toolRuntime.getTools().find(({ definition }) => definition.name === expectedDefinition.name);
1190
1357
  throw new AgentToolError(replacement ? "CONTRACT_MISMATCH" : "UNKNOWN_TOOL", replacement
1191
1358
  ? `Tool "${expectedDefinition.name}" changed after the model request was sent.`
@@ -1195,6 +1362,10 @@ class Nexface {
1195
1362
  if (inputErrors.length) {
1196
1363
  throw new AgentToolError("INVALID_INPUT", inputErrors.join(" "), { errors: inputErrors });
1197
1364
  }
1365
+ 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) };
1368
+ }
1198
1369
  const confirmation = runtimeTool?.confirmation;
1199
1370
  if (runtimeTool && confirmation) {
1200
1371
  if (confirmation.mode === "page"
@@ -1244,15 +1415,11 @@ class Nexface {
1244
1415
  runtimeTool.lifecycleSignal.addEventListener("abort", onRuntimeToolDisposed, { once: true });
1245
1416
  pending.removeInvalidationListener = () => runtimeTool.lifecycleSignal.removeEventListener("abort", onRuntimeToolDisposed);
1246
1417
  this.#pendingInterrupt = pending;
1247
- return { status: "interrupted", interrupt: descriptor };
1418
+ return { outcome: { type: "interrupt", interrupts: [{ id: descriptor.id, reason: "tool_call", toolCallId: descriptor.toolCallId, metadata: { nexface: { kind: "tool-approval", approval: descriptor } } }] } };
1248
1419
  }
1249
- output = runtimeTool && this.#toolRuntime
1420
+ output = registered ? await this.#executeRegistered(registered, toolInput, prepared.call.id, prepared.progressLabel, emit, signal) : runtimeTool && this.#toolRuntime
1250
1421
  ? await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(runtimeTool, toolInput, { signal, ...(prepared.progressLabel ? { progressLabel: prepared.progressLabel } : {}) })
1251
1422
  : await this.#toolBridge.execute(expectedDefinition, toolInput, { signal });
1252
- if (this.#externalToolBridge) {
1253
- bridgeExecutionConfirmed = true;
1254
- this.#bridgeExecutionOccurredInRun = true;
1255
- }
1256
1423
  }
1257
1424
  else if (prepared.kind === "native") {
1258
1425
  const expectedDefinition = nativeDefinitionsByName.get(prepared.call.name);
@@ -1270,14 +1437,17 @@ class Nexface {
1270
1437
  toolInput = parseArguments(prepared.call);
1271
1438
  const progressLabel = normalizeProgressLabel(toolInput.progressLabel);
1272
1439
  delete toolInput.progressLabel;
1440
+ if (this.#request) {
1441
+ const inputErrors = validateToolInput(expectedDefinition.inputSchema, toolInput);
1442
+ if (inputErrors.length)
1443
+ 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) };
1446
+ }
1273
1447
  const nativeRuntimeTool = this.#toolRuntime?.getTools().find((tool) => tool.definition === expectedDefinition);
1274
1448
  output = nativeRuntimeTool && this.#toolRuntime
1275
1449
  ? await getInternalToolRuntime(this.#toolRuntime).executeWithProgress(nativeRuntimeTool, toolInput, { signal, ...(progressLabel ? { progressLabel } : {}) })
1276
1450
  : await this.#toolBridge.execute(expectedDefinition, toolInput, { signal });
1277
- if (this.#externalToolBridge) {
1278
- bridgeExecutionConfirmed = true;
1279
- this.#bridgeExecutionOccurredInRun = true;
1280
- }
1281
1451
  if (prepared.call.name === "getSnapshot") {
1282
1452
  shouldLoadBrowserSkill = true;
1283
1453
  }
@@ -1285,27 +1455,12 @@ class Nexface {
1285
1455
  else {
1286
1456
  throw new AgentError("UNKNOWN_MODEL_TOOL", `Unknown model tool: ${prepared.call.name}.`);
1287
1457
  }
1288
- if (this.#externalToolBridge)
1289
- validateBridgeResult(output);
1290
1458
  const payload = { result: output ?? null };
1291
1459
  modelContent = modelToolPayload(prepared, payload);
1292
1460
  eventContent = serializeToolPayload(payload);
1293
1461
  }
1294
1462
  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
1463
  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
1464
  const cancelled = new AgentToolError("TOOL_CANCELLED", `Tool "${prepared.displayName}" was cancelled with the Agent run.`, prepared.call.name === "executeCode"
1310
1465
  ? { refsExpired: true }
1311
1466
  : undefined);
@@ -1342,14 +1497,6 @@ class Nexface {
1342
1497
  }
1343
1498
  throw signal.reason;
1344
1499
  }
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
1500
  failed = true;
1354
1501
  failedCallId = prepared.call.id;
1355
1502
  const payload = errorPayload(error);