@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.d.ts +2 -19
- package/dist/agent.js +494 -268
- package/dist/agent.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +13 -0
- package/dist/internal.js.map +1 -0
- package/dist/model/binding.d.ts +2 -0
- package/dist/model/binding.js.map +1 -1
- package/dist/models/42.js +23 -0
- package/dist/protocol-response.d.ts +17 -0
- package/dist/protocol-response.js +62 -0
- package/dist/protocol-response.js.map +1 -0
- package/dist/request.d.ts +99 -0
- package/dist/request.js +238 -0
- package/dist/request.js.map +1 -0
- package/dist/types.d.ts +29 -33
- package/dist/types.js.map +1 -1
- package/package.json +17 -3
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,
|
|
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,
|
|
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
|
|
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.
|
|
345
|
-
arguments: prepared.
|
|
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,
|
|
616
|
-
outcome: result.
|
|
617
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
707
|
-
|
|
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
|
|
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
|
-
|
|
723
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 (
|
|
780
|
-
|
|
881
|
+
if (source.kind === "request") {
|
|
882
|
+
this.#requestUsed = true;
|
|
883
|
+
this.#requestIdentity = { threadId: source.wire.threadId, runId };
|
|
781
884
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
905
|
+
else {
|
|
906
|
+
incoming = source.messages;
|
|
907
|
+
resume = source.resume;
|
|
791
908
|
}
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
852
|
-
signal.
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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?.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
#
|
|
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
|
-
|
|
1718
|
+
throw new AgentError("INVALID_RUN_INPUT", "The interrupt is no longer pending.");
|
|
1482
1719
|
this.#operation?.handle.setWaitingForUser(false);
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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:
|
|
1509
|
-
name:
|
|
1510
|
-
content: modelToolPayload(
|
|
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:
|
|
1517
|
-
content:
|
|
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
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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);
|