wtagent 0.4.0 → 1.0.0
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/README.md +161 -7
- package/package.json +5 -4
- package/src/artifacts/artifact-store.js +154 -0
- package/src/audio/music-artifact.js +66 -0
- package/src/audio/native-music-receiver.js +91 -0
- package/src/browser/base-web-adapter.js +354 -50
- package/src/browser/chatgpt-dom.js +179 -0
- package/src/browser/chatgpt-web-adapter.js +89 -125
- package/src/browser/fake-web-model-adapter.js +16 -1
- package/src/browser/gemini-web-adapter.js +61 -20
- package/src/browser/grok-web-adapter.js +35 -3
- package/src/browser/rendered-text.js +11 -0
- package/src/cli/i18n.js +40 -10
- package/src/cli/main.js +32 -3
- package/src/cli/prompt-input.js +68 -0
- package/src/image/browser-image-driver.js +238 -0
- package/src/image/browser-image-session-pool.js +96 -0
- package/src/image/image-generation-service.js +101 -0
- package/src/image/image-provider-driver.js +22 -0
- package/src/image/native-image-receiver.js +66 -0
- package/src/image/provider-registry.js +23 -0
- package/src/image/providers/chatgpt-image-driver.js +211 -0
- package/src/image/providers/gemini-image-driver.js +100 -0
- package/src/image/providers/grok-image-driver.js +193 -0
- package/src/policy/policy-engine.js +10 -5
- package/src/protocol/envelope.js +74 -0
- package/src/protocol/markers.js +16 -0
- package/src/protocol/prompt-builder.js +12 -9
- package/src/protocol/xml-protocol.js +36 -18
- package/src/runtime/agent-runtime.js +345 -65
- package/src/session/agent-session.js +73 -2
- package/src/tools/image-tools.js +92 -0
- package/src/tools/registry.js +13 -1
- package/docs/technical-design.md +0 -873
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
parseAgentResponse,
|
|
6
6
|
serializeProtocolError,
|
|
7
7
|
serializeToolResult,
|
|
8
|
-
stripUiNoiseLines,
|
|
9
8
|
} from "../protocol/xml-protocol.js";
|
|
10
9
|
import {
|
|
11
10
|
appendSystemReminder,
|
|
12
11
|
DEFAULT_SYSTEM_REMINDER,
|
|
12
|
+
NATIVE_IMAGE_SYSTEM_REMINDER,
|
|
13
13
|
} from "../protocol/markers.js";
|
|
14
14
|
import {
|
|
15
15
|
buildBootstrapPrompt,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
userMessage,
|
|
25
25
|
} from "../session/canonical-transcript.js";
|
|
26
26
|
import { DEFAULT_LIMITS } from "../shared/limits.js";
|
|
27
|
+
import { MUSIC_SYSTEM_REMINDER } from "../audio/native-music-receiver.js";
|
|
27
28
|
import { utf8ByteLength } from "../shared/text-budget.js";
|
|
28
29
|
import {
|
|
29
30
|
BrowserAdapterError,
|
|
@@ -157,6 +158,8 @@ export class AgentRuntime {
|
|
|
157
158
|
approval,
|
|
158
159
|
onEvent,
|
|
159
160
|
postAuthSetup = null,
|
|
161
|
+
nativeImageReceiver = null,
|
|
162
|
+
nativeMusicReceiver = null,
|
|
160
163
|
limits = DEFAULT_LIMITS,
|
|
161
164
|
}) {
|
|
162
165
|
this.adapter = adapter;
|
|
@@ -166,6 +169,8 @@ export class AgentRuntime {
|
|
|
166
169
|
this.approval = approval;
|
|
167
170
|
this.onEvent = onEvent;
|
|
168
171
|
this.postAuthSetup = postAuthSetup;
|
|
172
|
+
this.nativeImageReceiver = nativeImageReceiver;
|
|
173
|
+
this.nativeMusicReceiver = nativeMusicReceiver;
|
|
169
174
|
this.limits = limits;
|
|
170
175
|
this.conversationIdentityQueue = Promise.resolve();
|
|
171
176
|
this.deferConversationIdentityPersistence = false;
|
|
@@ -180,20 +185,40 @@ export class AgentRuntime {
|
|
|
180
185
|
return event;
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
#assertProviderReadyForMessage() {
|
|
189
|
+
if (this.session.state.pendingAuxiliaryTurn != null) {
|
|
190
|
+
throw new BrowserAdapterError(
|
|
191
|
+
"The provider operation has no verified assistant boundary. No further message was sent.",
|
|
192
|
+
{ code: "AUXILIARY_TURN_RECOVERY_REQUIRED", recoverable: false },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (this.session.state.pendingOutbound != null) {
|
|
196
|
+
throw new BrowserAdapterError(
|
|
197
|
+
"A previous provider message has not been reconciled. No further message was sent.",
|
|
198
|
+
{ code: "OUTBOUND_COMMIT_UNCERTAIN", recoverable: false },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
async sendMessage(text, {
|
|
184
204
|
files = [],
|
|
185
205
|
maxBytes = null,
|
|
186
206
|
allowFreshReconnect = false,
|
|
187
207
|
allowAssistantContinuation = false,
|
|
208
|
+
requireAttachments = false,
|
|
188
209
|
outboundKind = "runtime_message",
|
|
189
210
|
transcriptItems = [],
|
|
190
211
|
runtimeTurn = null,
|
|
191
212
|
pendingToolAcknowledgement = null,
|
|
192
213
|
} = {}) {
|
|
214
|
+
this.#assertProviderReadyForMessage();
|
|
215
|
+
await this.nativeMusicReceiver?.prepare();
|
|
193
216
|
const outboundId = randomUUID();
|
|
217
|
+
const reminder = this.nativeMusicReceiver ? MUSIC_SYSTEM_REMINDER
|
|
218
|
+
: this.nativeImageReceiver ? NATIVE_IMAGE_SYSTEM_REMINDER : DEFAULT_SYSTEM_REMINDER;
|
|
194
219
|
const message = appendSystemReminder(
|
|
195
220
|
text,
|
|
196
|
-
`${
|
|
221
|
+
`${reminder}${OUTBOUND_CORRELATION_PREFIX}${outboundId}.`,
|
|
197
222
|
);
|
|
198
223
|
const pendingOutbound = {
|
|
199
224
|
kind: outboundKind,
|
|
@@ -213,6 +238,7 @@ export class AgentRuntime {
|
|
|
213
238
|
maxBytes,
|
|
214
239
|
allowFreshReconnect,
|
|
215
240
|
allowAssistantContinuation,
|
|
241
|
+
requireAttachments,
|
|
216
242
|
outboundId,
|
|
217
243
|
});
|
|
218
244
|
const handoff = await this.#queueSessionOperation(() => (
|
|
@@ -414,6 +440,7 @@ export class AgentRuntime {
|
|
|
414
440
|
maxBytes,
|
|
415
441
|
allowFreshReconnect,
|
|
416
442
|
allowAssistantContinuation,
|
|
443
|
+
requireAttachments,
|
|
417
444
|
outboundId,
|
|
418
445
|
}) {
|
|
419
446
|
try {
|
|
@@ -422,6 +449,7 @@ export class AgentRuntime {
|
|
|
422
449
|
maxBytes,
|
|
423
450
|
outboundId,
|
|
424
451
|
allowAssistantContinuation,
|
|
452
|
+
requireAttachments,
|
|
425
453
|
});
|
|
426
454
|
} catch (error) {
|
|
427
455
|
if (isConnectionLostError(error)) {
|
|
@@ -447,11 +475,13 @@ export class AgentRuntime {
|
|
|
447
475
|
)
|
|
448
476
|
),
|
|
449
477
|
});
|
|
478
|
+
await this.nativeMusicReceiver?.prepare();
|
|
450
479
|
return await this.adapter.sendMessage(message, {
|
|
451
480
|
files,
|
|
452
481
|
maxBytes,
|
|
453
482
|
outboundId,
|
|
454
483
|
allowAssistantContinuation,
|
|
484
|
+
requireAttachments,
|
|
455
485
|
});
|
|
456
486
|
}
|
|
457
487
|
throw error;
|
|
@@ -515,6 +545,9 @@ export class AgentRuntime {
|
|
|
515
545
|
if (state.pendingAssistantTurn != null) {
|
|
516
546
|
blockers.push("pending assistant handoff");
|
|
517
547
|
}
|
|
548
|
+
if (state.pendingAuxiliaryTurn != null) {
|
|
549
|
+
blockers.push("pending auxiliary provider turn");
|
|
550
|
+
}
|
|
518
551
|
if (state.pendingToolResult != null) {
|
|
519
552
|
blockers.push("pending tool result");
|
|
520
553
|
}
|
|
@@ -581,7 +614,8 @@ export class AgentRuntime {
|
|
|
581
614
|
|
|
582
615
|
buildToolResultMessage(result, { suffix = "" } = {}) {
|
|
583
616
|
const limit = this.limits.maxBrowserToolResultBytes;
|
|
584
|
-
const nonResultBytes = utf8ByteLength(appendSystemReminder(suffix
|
|
617
|
+
const nonResultBytes = utf8ByteLength(appendSystemReminder(suffix,
|
|
618
|
+
this.nativeImageReceiver ? NATIVE_IMAGE_SYSTEM_REMINDER : DEFAULT_SYSTEM_REMINDER))
|
|
585
619
|
+ OUTBOUND_CORRELATION_RESERVE_BYTES;
|
|
586
620
|
const resultBudget = limit - nonResultBytes;
|
|
587
621
|
if (resultBudget < 512) {
|
|
@@ -611,6 +645,144 @@ export class AgentRuntime {
|
|
|
611
645
|
);
|
|
612
646
|
}
|
|
613
647
|
|
|
648
|
+
// Runs a provider-native helper turn (currently used by image.generate) in
|
|
649
|
+
// the same web conversation without exposing that turn as another WTAgent
|
|
650
|
+
// protocol exchange. The send still goes through the adapter's full causal
|
|
651
|
+
// checks, and both provider message identities are checkpointed before the
|
|
652
|
+
// caller is allowed to send the tool result.
|
|
653
|
+
async runAuxiliaryTurn({
|
|
654
|
+
text,
|
|
655
|
+
files = [],
|
|
656
|
+
timeoutMs = this.limits.modelTurnTimeoutMs,
|
|
657
|
+
waitForCompletion,
|
|
658
|
+
}) {
|
|
659
|
+
if (typeof waitForCompletion !== "function") {
|
|
660
|
+
throw new TypeError("An auxiliary turn requires a completion callback.");
|
|
661
|
+
}
|
|
662
|
+
this.#assertProviderReadyForMessage();
|
|
663
|
+
const outboundId = randomUUID();
|
|
664
|
+
const message = appendSystemReminder(
|
|
665
|
+
text,
|
|
666
|
+
`This is an internal WTAgent provider operation. Perform only the requested operation; do not emit the WTAgent XML protocol.${OUTBOUND_CORRELATION_PREFIX}${outboundId}.`,
|
|
667
|
+
);
|
|
668
|
+
const pendingOutbound = {
|
|
669
|
+
kind: "auxiliary_tool_turn",
|
|
670
|
+
outboundId,
|
|
671
|
+
messageHash: createHash("sha256").update(message).digest("hex"),
|
|
672
|
+
preparedAt: new Date().toISOString(),
|
|
673
|
+
transcriptItems: [],
|
|
674
|
+
runtimeTurn: null,
|
|
675
|
+
pendingToolAcknowledgement: null,
|
|
676
|
+
conversationUrl: this.session.state.conversationUrl ?? null,
|
|
677
|
+
conversationTargetId: this.session.state.conversationTargetId ?? null,
|
|
678
|
+
};
|
|
679
|
+
await this.#queueSessionUpdate({ pendingOutbound });
|
|
680
|
+
|
|
681
|
+
let sendResult;
|
|
682
|
+
try {
|
|
683
|
+
sendResult = await this.#sendMessageWithReconnect(message, {
|
|
684
|
+
files,
|
|
685
|
+
maxBytes: null,
|
|
686
|
+
allowFreshReconnect: false,
|
|
687
|
+
allowAssistantContinuation: false,
|
|
688
|
+
requireAttachments: files.length > 0,
|
|
689
|
+
outboundId,
|
|
690
|
+
});
|
|
691
|
+
await this.#queueSessionOperation(() => (
|
|
692
|
+
this.session.commitPendingAuxiliaryTurn({
|
|
693
|
+
outboundId,
|
|
694
|
+
turn: {
|
|
695
|
+
conversationUrl: sendResult?.conversationUrl
|
|
696
|
+
?? this.session.state.conversationUrl,
|
|
697
|
+
conversationTargetId: sendResult?.conversationTargetId
|
|
698
|
+
?? this.session.state.conversationTargetId,
|
|
699
|
+
userMessageId: sendResult?.userMessageId ?? null,
|
|
700
|
+
userTurn: sendResult?.userTurn ?? null,
|
|
701
|
+
assistantBaseline: sendResult?.assistantBaseline ?? null,
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
const sendStatus = this.adapter.getLastSendStatus?.() ?? "commit-unknown";
|
|
707
|
+
await this.#queueSessionOperation(async () => {
|
|
708
|
+
if (this.session.state.pendingOutbound?.outboundId === outboundId) {
|
|
709
|
+
await this.session.update({
|
|
710
|
+
pendingOutbound: sendStatus === "not-submitted"
|
|
711
|
+
? null
|
|
712
|
+
: {
|
|
713
|
+
...pendingOutbound,
|
|
714
|
+
status: "commit-unknown",
|
|
715
|
+
failedAt: new Date().toISOString(),
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}).catch(() => {});
|
|
720
|
+
if (sendStatus !== "not-submitted") {
|
|
721
|
+
error.completionUnknown = true;
|
|
722
|
+
}
|
|
723
|
+
throw error;
|
|
724
|
+
} finally {
|
|
725
|
+
await this.#syncConversationIdentity({ suppressErrors: true });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let value;
|
|
729
|
+
try {
|
|
730
|
+
value = await waitForCompletion({
|
|
731
|
+
adapter: this.adapter,
|
|
732
|
+
page: this.adapter.page,
|
|
733
|
+
sendResult,
|
|
734
|
+
timeoutMs,
|
|
735
|
+
});
|
|
736
|
+
const completion = await this.adapter.captureAuxiliaryTurnCompletion?.({
|
|
737
|
+
timeoutMs: Math.min(timeoutMs, 30_000),
|
|
738
|
+
})
|
|
739
|
+
?? null;
|
|
740
|
+
const assistantMessageId = completion?.assistantMessageId
|
|
741
|
+
?? await this.adapter.getLastAssistantMessageId?.()
|
|
742
|
+
?? null;
|
|
743
|
+
const assistantTurn = completion?.assistantTurn
|
|
744
|
+
?? await this.adapter.getLastAssistantTurn?.()
|
|
745
|
+
?? null;
|
|
746
|
+
await this.#queueSessionOperation(() => (
|
|
747
|
+
this.session.completePendingAuxiliaryTurn({
|
|
748
|
+
outboundId,
|
|
749
|
+
assistantMessageId,
|
|
750
|
+
assistantTurn,
|
|
751
|
+
})
|
|
752
|
+
));
|
|
753
|
+
return value;
|
|
754
|
+
} catch (error) {
|
|
755
|
+
// A provider can finish the visible assistant turn yet fail while WTAgent
|
|
756
|
+
// downloads or validates its artifact. Close that browser-turn checkpoint
|
|
757
|
+
// when its final boundary is still provable; otherwise retain it so a
|
|
758
|
+
// later resume cannot blindly submit a duplicate image request.
|
|
759
|
+
try {
|
|
760
|
+
const completion = await this.adapter.captureAuxiliaryTurnCompletion?.({
|
|
761
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
762
|
+
});
|
|
763
|
+
if (completion) {
|
|
764
|
+
await this.#queueSessionOperation(() => (
|
|
765
|
+
this.session.completePendingAuxiliaryTurn({
|
|
766
|
+
outboundId,
|
|
767
|
+
assistantMessageId: completion.assistantMessageId ?? null,
|
|
768
|
+
assistantTurn: completion.assistantTurn ?? null,
|
|
769
|
+
})
|
|
770
|
+
));
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
// Keep pendingAuxiliaryTurn intact when completion cannot be proven.
|
|
774
|
+
}
|
|
775
|
+
// If the completion callback produced a temporary artifact but the
|
|
776
|
+
// assistant-boundary checkpoint failed, ownership never reaches the
|
|
777
|
+
// ImageGenerationService, so clean it here.
|
|
778
|
+
await value?.cleanup?.().catch(() => null);
|
|
779
|
+
error.completionUnknown = true;
|
|
780
|
+
throw error;
|
|
781
|
+
} finally {
|
|
782
|
+
await this.#syncConversationIdentity({ suppressErrors: true });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
614
786
|
async #appendHandoffTranscript(activeHandoff, suffix, item) {
|
|
615
787
|
if (!activeHandoff?.handoffId) {
|
|
616
788
|
throw new Error("Cannot append transcript without an assistant handoff ID.");
|
|
@@ -789,7 +961,9 @@ export class AgentRuntime {
|
|
|
789
961
|
));
|
|
790
962
|
}
|
|
791
963
|
|
|
792
|
-
|
|
964
|
+
// Recovery reconciles text/identity. A not-yet-checkpointed native turn
|
|
965
|
+
// still needs the ordinary image-aware completion reader before sealing it.
|
|
966
|
+
if (outcome.status === "complete" && ((!this.nativeImageReceiver && !this.nativeMusicReceiver) || handoff.status === "complete")) {
|
|
793
967
|
const outcomeHash = createHash("sha256")
|
|
794
968
|
.update(outcome.rawResponse)
|
|
795
969
|
.digest("hex");
|
|
@@ -805,6 +979,8 @@ export class AgentRuntime {
|
|
|
805
979
|
assistantMessageId: outcome.assistantMessageId ?? null,
|
|
806
980
|
assistantTurn: outcome.assistantTurn ?? null,
|
|
807
981
|
rawResponse: outcome.rawResponse,
|
|
982
|
+
nativeImages: handoff.nativeImages ?? [],
|
|
983
|
+
nativeMusic: handoff.nativeMusic ?? [],
|
|
808
984
|
})
|
|
809
985
|
));
|
|
810
986
|
}
|
|
@@ -824,8 +1000,23 @@ export class AgentRuntime {
|
|
|
824
1000
|
const previousConversationUrl = this.session.state.conversationUrl;
|
|
825
1001
|
const pendingOutboundAtStart = this.session.state.pendingOutbound;
|
|
826
1002
|
const pendingAssistantAtStart = this.session.state.pendingAssistantTurn;
|
|
1003
|
+
const pendingAuxiliaryAtStart = this.session.state.pendingAuxiliaryTurn;
|
|
1004
|
+
if (pendingAuxiliaryAtStart != null) {
|
|
1005
|
+
throw new BrowserAdapterError(
|
|
1006
|
+
"An image/provider auxiliary turn was interrupted after submission. "
|
|
1007
|
+
+ "WTAgent will not replay it automatically because that could create a duplicate remote artifact.",
|
|
1008
|
+
{ code: "AUXILIARY_TURN_RECOVERY_REQUIRED", recoverable: false },
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
827
1011
|
const hasPendingRecovery = pendingOutboundAtStart != null
|
|
828
1012
|
|| pendingAssistantAtStart != null;
|
|
1013
|
+
if (this.nativeMusicReceiver && resume && !hasPendingRecovery
|
|
1014
|
+
&& !instruction?.trim() && files.length === 0 && this.session.state.lastMusicResult) {
|
|
1015
|
+
// Bare resume of a completed generation must not spend quota again.
|
|
1016
|
+
const result = this.session.state.lastMusicResult;
|
|
1017
|
+
await this.emit("run.completed", result);
|
|
1018
|
+
return { sessionId: this.session.sessionId, ...result };
|
|
1019
|
+
}
|
|
829
1020
|
const supportsStrictRecovery = Boolean(
|
|
830
1021
|
this.adapter.supportsPendingOutboundRecovery?.(),
|
|
831
1022
|
);
|
|
@@ -1091,7 +1282,13 @@ export class AgentRuntime {
|
|
|
1091
1282
|
// records only confirmed user messages; WTAgent scaffolding stays transport-only.
|
|
1092
1283
|
let initialTranscript = [];
|
|
1093
1284
|
if (!legacyAssistantRecoveryAtStart) {
|
|
1094
|
-
if (
|
|
1285
|
+
if (this.nativeMusicReceiver) {
|
|
1286
|
+
if (inPlaceRecovery) throw new Error("Music generation is not automatically repeated; resume the pending turn without a new instruction.");
|
|
1287
|
+
const request = effectiveInstruction?.trim() || task;
|
|
1288
|
+
initialMessage = `Create the following music using the native Music tool.\n\n${request}`;
|
|
1289
|
+
initialTranscript = resume && effectiveInstruction?.trim() ? [userMessage(effectiveInstruction.trim(), messageOptions)] : [];
|
|
1290
|
+
initialKind = resume ? "music_follow_up" : "music_generation";
|
|
1291
|
+
} else if (resumedExistingConversation && pendingToolResult && !inPlaceRecovery) {
|
|
1095
1292
|
let suffix = "";
|
|
1096
1293
|
if (instruction?.trim()) {
|
|
1097
1294
|
suffix = `\n<resume_instruction>${cdata(instruction)}</resume_instruction>`;
|
|
@@ -1120,6 +1317,7 @@ export class AgentRuntime {
|
|
|
1120
1317
|
: instruction,
|
|
1121
1318
|
state: this.session.state,
|
|
1122
1319
|
tools: this.registry.list(),
|
|
1320
|
+
nativeImages: Boolean(this.nativeImageReceiver),
|
|
1123
1321
|
});
|
|
1124
1322
|
initialMessage = prompt.web;
|
|
1125
1323
|
// The original task is already the one safe canonical item required by the
|
|
@@ -1135,6 +1333,7 @@ export class AgentRuntime {
|
|
|
1135
1333
|
task,
|
|
1136
1334
|
projectRoot,
|
|
1137
1335
|
tools: this.registry.list(),
|
|
1336
|
+
nativeImages: Boolean(this.nativeImageReceiver),
|
|
1138
1337
|
});
|
|
1139
1338
|
initialMessage = prompt.web;
|
|
1140
1339
|
// The opening item was persisted before browser setup above.
|
|
@@ -1188,12 +1387,16 @@ export class AgentRuntime {
|
|
|
1188
1387
|
: baseTurn + step;
|
|
1189
1388
|
await this.session.update({ turn: turnNumber, phase: "waiting_model" });
|
|
1190
1389
|
let raw;
|
|
1390
|
+
let nativeImages = [];
|
|
1391
|
+
let nativeMusic = [];
|
|
1191
1392
|
let emptyAssistantRetries = 0;
|
|
1192
1393
|
let connectionRetries = 0;
|
|
1193
1394
|
for (;;) {
|
|
1194
1395
|
try {
|
|
1195
1396
|
if (activeHandoff.status === "complete") {
|
|
1196
1397
|
raw = activeHandoff.rawResponse;
|
|
1398
|
+
nativeImages = activeHandoff.nativeImages ?? [];
|
|
1399
|
+
nativeMusic = activeHandoff.nativeMusic ?? [];
|
|
1197
1400
|
} else {
|
|
1198
1401
|
try {
|
|
1199
1402
|
raw = await this.adapter.waitForTurnComplete({
|
|
@@ -1201,6 +1404,10 @@ export class AgentRuntime {
|
|
|
1201
1404
|
stableWindowMs: this.limits.modelStableWindowMs,
|
|
1202
1405
|
emptyResponseWindowMs: this.limits.emptyAssistantWindowMs,
|
|
1203
1406
|
deadRequestGraceMs: this.limits.deadRequestGraceMs,
|
|
1407
|
+
readNativeImages: this.nativeImageReceiver
|
|
1408
|
+
? (message) => this.nativeImageReceiver.read(message) : null,
|
|
1409
|
+
readNativeMusic: this.nativeMusicReceiver
|
|
1410
|
+
? (message) => this.nativeMusicReceiver.read(message) : null,
|
|
1204
1411
|
onDelta: async (delta) => {
|
|
1205
1412
|
await this.onEvent?.({
|
|
1206
1413
|
type: "model.streaming",
|
|
@@ -1210,6 +1417,13 @@ export class AgentRuntime {
|
|
|
1210
1417
|
});
|
|
1211
1418
|
},
|
|
1212
1419
|
});
|
|
1420
|
+
if (raw && typeof raw === "object" && Array.isArray(raw.nativeImages)) {
|
|
1421
|
+
nativeImages = raw.nativeImages;
|
|
1422
|
+
raw = raw.text ?? "";
|
|
1423
|
+
} else if (raw && typeof raw === "object" && Array.isArray(raw.nativeMusic)) {
|
|
1424
|
+
nativeMusic = raw.nativeMusic;
|
|
1425
|
+
raw = raw.text ?? "";
|
|
1426
|
+
}
|
|
1213
1427
|
} finally {
|
|
1214
1428
|
// Navigation events normally persist canonicalization immediately;
|
|
1215
1429
|
// this validated fallback runs on every outcome but never masks a
|
|
@@ -1259,6 +1473,7 @@ export class AgentRuntime {
|
|
|
1259
1473
|
|
|
1260
1474
|
const deadRequest = error?.code === "DEAD_ASSISTANT_REQUEST";
|
|
1261
1475
|
const generationFailed = error?.code === "GENERATION_FAILED";
|
|
1476
|
+
if (this.nativeMusicReceiver) throw error;
|
|
1262
1477
|
if (
|
|
1263
1478
|
error?.code !== "EMPTY_ASSISTANT_RESPONSE"
|
|
1264
1479
|
&& !deadRequest
|
|
@@ -1351,6 +1566,8 @@ export class AgentRuntime {
|
|
|
1351
1566
|
assistantMessageId,
|
|
1352
1567
|
assistantTurn,
|
|
1353
1568
|
rawResponse: raw,
|
|
1569
|
+
nativeImages,
|
|
1570
|
+
nativeMusic,
|
|
1354
1571
|
})
|
|
1355
1572
|
));
|
|
1356
1573
|
} else {
|
|
@@ -1372,6 +1589,66 @@ export class AgentRuntime {
|
|
|
1372
1589
|
assistantMessageId,
|
|
1373
1590
|
});
|
|
1374
1591
|
|
|
1592
|
+
if (this.nativeMusicReceiver) {
|
|
1593
|
+
let artifacts = activeHandoff.nativeArtifacts;
|
|
1594
|
+
if (!artifacts) {
|
|
1595
|
+
artifacts = nativeMusic.length ? await this.nativeMusicReceiver.save(nativeMusic, {
|
|
1596
|
+
projectRoot, handoffId: activeHandoff.handoffId, assistantMessageId,
|
|
1597
|
+
}) : [];
|
|
1598
|
+
activeHandoff = await this.#queueSessionOperation(() => (
|
|
1599
|
+
this.session.refreshPendingAssistantTurn(activeHandoff.handoffId, { nativeArtifacts: artifacts })
|
|
1600
|
+
));
|
|
1601
|
+
}
|
|
1602
|
+
const message = [
|
|
1603
|
+
raw,
|
|
1604
|
+
...artifacts.map((artifact) => `Saved music: ${artifact.localPath} (${artifact.durationSeconds.toFixed(2)} s)`),
|
|
1605
|
+
...artifacts.map((artifact) => artifact.audioExtractionNote).filter(Boolean),
|
|
1606
|
+
...(artifacts.length ? [] : ["No music file was generated or saved."]),
|
|
1607
|
+
].filter(Boolean).join("\n");
|
|
1608
|
+
await this.#appendHandoffTranscript(activeHandoff, "assistant", assistantMessage(message));
|
|
1609
|
+
const result = { message, artifacts };
|
|
1610
|
+
await this.#queueSessionOperation(() => this.session.clearPendingAssistantTurn(activeHandoff.handoffId, {
|
|
1611
|
+
phase: "idle", lastMessage: message, pendingToolResult: null, lastMusicResult: result,
|
|
1612
|
+
}));
|
|
1613
|
+
if (artifacts.length) await this.emit("model.native_music", { turn: turnNumber, artifacts });
|
|
1614
|
+
await this.emit("run.completed", result);
|
|
1615
|
+
return { sessionId: this.session.sessionId, ...result };
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if (nativeImages.length > 0) {
|
|
1619
|
+
if (!this.nativeImageReceiver) {
|
|
1620
|
+
throw new Error("This session has pending native images but no image receiver.");
|
|
1621
|
+
}
|
|
1622
|
+
let artifacts = activeHandoff.nativeArtifacts;
|
|
1623
|
+
if (!artifacts) {
|
|
1624
|
+
artifacts = await this.nativeImageReceiver.save(nativeImages, {
|
|
1625
|
+
projectRoot,
|
|
1626
|
+
handoffId: activeHandoff.handoffId,
|
|
1627
|
+
assistantMessageId,
|
|
1628
|
+
});
|
|
1629
|
+
activeHandoff = await this.#queueSessionOperation(() => (
|
|
1630
|
+
this.session.refreshPendingAssistantTurn(activeHandoff.handoffId, { nativeArtifacts: artifacts })
|
|
1631
|
+
));
|
|
1632
|
+
}
|
|
1633
|
+
protocolErrors = 0;
|
|
1634
|
+
const message = [raw, ...artifacts.map((artifact) => `Saved image: ${artifact.localPath}`)]
|
|
1635
|
+
.filter(Boolean).join("\n");
|
|
1636
|
+
await this.#appendHandoffTranscript(activeHandoff, "assistant", assistantMessage(message));
|
|
1637
|
+
await this.emit("model.native_images", { turn: turnNumber, artifacts });
|
|
1638
|
+
await this.emit("model.progress", { turn: turnNumber, message });
|
|
1639
|
+
const receipt = artifacts.map(({ localPath, mimeType, width, height, size, sha256 }) => (
|
|
1640
|
+
{ localPath, mimeType, width, height, size, sha256 }
|
|
1641
|
+
));
|
|
1642
|
+
await this.sendMessage(
|
|
1643
|
+
`<native_image_result>${cdata(JSON.stringify(receipt))}</native_image_result>\n`
|
|
1644
|
+
+ "The images from your preceding reply are saved at these verified local paths. "
|
|
1645
|
+
+ "Continue the user's original task. Generate further images directly if needed; "
|
|
1646
|
+
+ "use local tools for file operations, or finish with a done=true XML answer.",
|
|
1647
|
+
{ outboundKind: "native_image_result", runtimeTurn: turnNumber + 1 },
|
|
1648
|
+
);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1375
1652
|
let parsed;
|
|
1376
1653
|
let finalMessage;
|
|
1377
1654
|
let usedTrailingProse = false;
|
|
@@ -1421,50 +1698,33 @@ export class AgentRuntime {
|
|
|
1421
1698
|
);
|
|
1422
1699
|
}
|
|
1423
1700
|
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
//
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
.
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
await this.emit("protocol.plain_answer", {
|
|
1441
|
-
snippet: plainAnswer.slice(0, 200),
|
|
1442
|
-
});
|
|
1443
|
-
await this.#appendHandoffTranscript(
|
|
1444
|
-
activeHandoff,
|
|
1445
|
-
"assistant",
|
|
1446
|
-
assistantMessage(plainAnswer),
|
|
1701
|
+
// A finished native refusal/clarification is data for the agent, not a
|
|
1702
|
+
// successful image or an empty-response retry. Keep a bounded budget;
|
|
1703
|
+
// malformed XML still follows strict protocol correction below.
|
|
1704
|
+
if (this.nativeImageReceiver && raw.trim() && !/<\/?(?:agent_response|tool_call|tool_calls|invoke|done|message)(?:\s|>)/i.test(raw)) {
|
|
1705
|
+
protocolErrors += 1;
|
|
1706
|
+
await this.#appendHandoffTranscript(activeHandoff, "assistant", assistantMessage(raw));
|
|
1707
|
+
await this.emit("model.progress", { turn: turnNumber, message: raw });
|
|
1708
|
+
if (protocolErrors >= this.limits.maxProtocolErrors) {
|
|
1709
|
+
throw new ProtocolError("The provider repeatedly returned text without a completed XML answer or an image.");
|
|
1710
|
+
}
|
|
1711
|
+
await this.sendMessage(
|
|
1712
|
+
`<native_image_result type="text">${cdata(raw)}</native_image_result>\n`
|
|
1713
|
+
+ "Your preceding reply contained only this text; no image was received or saved. "
|
|
1714
|
+
+ "Continue the original task based on this result. If generation is unavailable or declined, "
|
|
1715
|
+
+ "explain it or ask the user a specific question in a done=true XML answer. Do not automatically retry the same image request.",
|
|
1716
|
+
{ outboundKind: "native_image_text", runtimeTurn: turnNumber + 1 },
|
|
1447
1717
|
);
|
|
1448
|
-
|
|
1449
|
-
this.session.clearPendingAssistantTurn(
|
|
1450
|
-
activeHandoff.handoffId,
|
|
1451
|
-
{
|
|
1452
|
-
phase: "idle",
|
|
1453
|
-
lastMessage: plainAnswer,
|
|
1454
|
-
pendingToolResult: null,
|
|
1455
|
-
},
|
|
1456
|
-
)
|
|
1457
|
-
));
|
|
1458
|
-
await this.emit("run.completed", {
|
|
1459
|
-
message: plainAnswer,
|
|
1460
|
-
plainAnswer: true,
|
|
1461
|
-
});
|
|
1462
|
-
return {
|
|
1463
|
-
sessionId: this.session.sessionId,
|
|
1464
|
-
message: plainAnswer,
|
|
1465
|
-
};
|
|
1718
|
+
continue;
|
|
1466
1719
|
}
|
|
1467
1720
|
|
|
1721
|
+
// Absence of an envelope is a protocol failure, not proof of done.
|
|
1722
|
+
// This also covers UI-only text and partial native-artifact responses.
|
|
1723
|
+
await this.adapter.writeDiagnostics?.("protocol-invalid", {
|
|
1724
|
+
turn: turnNumber,
|
|
1725
|
+
assistantMessageId,
|
|
1726
|
+
error: error.message,
|
|
1727
|
+
}).catch(() => null);
|
|
1468
1728
|
protocolErrors += 1;
|
|
1469
1729
|
await this.emit("protocol.invalid", {
|
|
1470
1730
|
message: error.message,
|
|
@@ -1628,6 +1888,7 @@ export class AgentRuntime {
|
|
|
1628
1888
|
const sideEffect = isReadTool ? null : identity;
|
|
1629
1889
|
const fingerprint = identity.fingerprint;
|
|
1630
1890
|
let result;
|
|
1891
|
+
let restoredForTool = false;
|
|
1631
1892
|
|
|
1632
1893
|
if (
|
|
1633
1894
|
sideEffect
|
|
@@ -1746,32 +2007,46 @@ export class AgentRuntime {
|
|
|
1746
2007
|
}
|
|
1747
2008
|
|
|
1748
2009
|
if (!result) {
|
|
2010
|
+
if (preparedCall.definition.requiresVisibleBrowser) {
|
|
2011
|
+
await this.adapter.restoreWindow?.();
|
|
2012
|
+
restoredForTool = true;
|
|
2013
|
+
}
|
|
1749
2014
|
await this.emit("tool.started", {
|
|
1750
2015
|
id: preparedCall.id,
|
|
1751
2016
|
name: preparedCall.name,
|
|
1752
2017
|
});
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
type: "tool.output",
|
|
1765
|
-
sessionId: this.session.sessionId,
|
|
1766
|
-
timestamp: new Date().toISOString(),
|
|
1767
|
-
payload: {
|
|
2018
|
+
try {
|
|
2019
|
+
result = await this.registry.execute(preparedCall, {
|
|
2020
|
+
projectRoot,
|
|
2021
|
+
allowOutside: grants?.allowOutside ?? false,
|
|
2022
|
+
toolTimeoutMs: this.limits.toolTimeoutMs,
|
|
2023
|
+
sessionId: this.session.sessionId,
|
|
2024
|
+
operationKey: identity.operationKey,
|
|
2025
|
+
requestSignature: identity.requestSignature,
|
|
2026
|
+
runAuxiliaryTurn: (options) => this.runAuxiliaryTurn(options),
|
|
2027
|
+
onToolOutput: async (output) => {
|
|
2028
|
+
await this.session.appendToolOutput({
|
|
1768
2029
|
id: preparedCall.id,
|
|
1769
2030
|
name: preparedCall.name,
|
|
1770
2031
|
...output,
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2032
|
+
});
|
|
2033
|
+
await this.onEvent?.({
|
|
2034
|
+
type: "tool.output",
|
|
2035
|
+
sessionId: this.session.sessionId,
|
|
2036
|
+
timestamp: new Date().toISOString(),
|
|
2037
|
+
payload: {
|
|
2038
|
+
id: preparedCall.id,
|
|
2039
|
+
name: preparedCall.name,
|
|
2040
|
+
...output,
|
|
2041
|
+
},
|
|
2042
|
+
});
|
|
2043
|
+
},
|
|
2044
|
+
});
|
|
2045
|
+
} finally {
|
|
2046
|
+
if (restoredForTool) {
|
|
2047
|
+
await this.adapter.page?.bringToFront?.().catch(() => null);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1775
2050
|
}
|
|
1776
2051
|
|
|
1777
2052
|
if (sideEffect) {
|
|
@@ -1794,6 +2069,8 @@ export class AgentRuntime {
|
|
|
1794
2069
|
}
|
|
1795
2070
|
}
|
|
1796
2071
|
|
|
2072
|
+
this.#assertProviderReadyForMessage();
|
|
2073
|
+
|
|
1797
2074
|
await this.#appendHandoffTranscript(
|
|
1798
2075
|
activeHandoff,
|
|
1799
2076
|
"function_output",
|
|
@@ -1809,6 +2086,9 @@ export class AgentRuntime {
|
|
|
1809
2086
|
name: result.name,
|
|
1810
2087
|
ok: result.ok,
|
|
1811
2088
|
});
|
|
2089
|
+
if (restoredForTool) {
|
|
2090
|
+
await this.adapter.minimizeWindow?.();
|
|
2091
|
+
}
|
|
1812
2092
|
}
|
|
1813
2093
|
|
|
1814
2094
|
}
|