@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 +2 -19
- package/dist/agent.js +243 -96
- package/dist/agent.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 +2 -0
- package/dist/internal.js +12 -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 +22 -0
- package/dist/request.d.ts +60 -0
- package/dist/request.js +162 -0
- package/dist/request.js.map +1 -0
- package/dist/types.d.ts +23 -24
- package/dist/types.js.map +1 -1
- package/package.json +6 -2
package/dist/agent.d.ts
CHANGED
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
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,
|
|
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,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,
|
|
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 }), ...(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
|
-
#
|
|
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
|
-
|
|
707
|
-
|
|
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
|
|
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
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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);
|