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