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
|
@@ -115,7 +115,10 @@ async function acquirePersistenceLockMutex(
|
|
|
115
115
|
try {
|
|
116
116
|
servers.push(await openPersistenceLockServer({ port, token }));
|
|
117
117
|
} catch (error) {
|
|
118
|
-
|
|
118
|
+
// Windows excluded port ranges fail with EACCES rather than
|
|
119
|
+
// EADDRINUSE. Both make this member unavailable; the same fixed
|
|
120
|
+
// majority is still required, so contenders cannot bypass the lock.
|
|
121
|
+
if (error.code !== "EADDRINUSE" && error.code !== "EACCES") {
|
|
119
122
|
throw error;
|
|
120
123
|
}
|
|
121
124
|
lastError = error;
|
|
@@ -560,7 +563,7 @@ export class AgentSession {
|
|
|
560
563
|
this.persistenceQueue = Promise.resolve();
|
|
561
564
|
}
|
|
562
565
|
|
|
563
|
-
static async create({ sessionsDir, tasksDir, task, projectRoot, mode, provider = "chatgpt" }) {
|
|
566
|
+
static async create({ sessionsDir, tasksDir, task, projectRoot, mode, provider = "chatgpt", generationType = "agent" }) {
|
|
564
567
|
const root = await ensureSessionsRoot(sessionsDir ?? tasksDir);
|
|
565
568
|
const sessionId = `session_${new Date().toISOString().replaceAll(/[-:.TZ]/g, "").slice(0, 14)}_${randomUUID().slice(0, 8)}`;
|
|
566
569
|
const directory = path.join(root, sessionId);
|
|
@@ -574,6 +577,7 @@ export class AgentSession {
|
|
|
574
577
|
projectRoot: path.resolve(projectRoot),
|
|
575
578
|
provider,
|
|
576
579
|
mode,
|
|
580
|
+
generationType,
|
|
577
581
|
phase: "idle",
|
|
578
582
|
turn: 0,
|
|
579
583
|
runCount: 0,
|
|
@@ -583,6 +587,7 @@ export class AgentSession {
|
|
|
583
587
|
lastAssistantMessageId: null,
|
|
584
588
|
pendingOutbound: null,
|
|
585
589
|
pendingAssistantTurn: null,
|
|
590
|
+
pendingAuxiliaryTurn: null,
|
|
586
591
|
activeMode: null,
|
|
587
592
|
stateRevision: 0,
|
|
588
593
|
createdAt: now,
|
|
@@ -657,6 +662,7 @@ export class AgentSession {
|
|
|
657
662
|
state.lastAssistantMessageId ??= null;
|
|
658
663
|
state.pendingOutbound ??= null;
|
|
659
664
|
state.pendingAssistantTurn ??= null;
|
|
665
|
+
state.pendingAuxiliaryTurn ??= null;
|
|
660
666
|
state.activeMode ??= null;
|
|
661
667
|
state.stateRevision = stateRevision(state);
|
|
662
668
|
state.rolloutFile ??= "transcript.jsonl";
|
|
@@ -1130,6 +1136,63 @@ export class AgentSession {
|
|
|
1130
1136
|
});
|
|
1131
1137
|
}
|
|
1132
1138
|
|
|
1139
|
+
async commitPendingAuxiliaryTurn({ outboundId, turn = {} }) {
|
|
1140
|
+
if (typeof outboundId !== "string" || outboundId.length === 0) {
|
|
1141
|
+
throw new Error("A pending outbound ID is required for an auxiliary turn.");
|
|
1142
|
+
}
|
|
1143
|
+
return await this.#mutateState((nextState) => {
|
|
1144
|
+
if (nextState.pendingOutbound?.outboundId !== outboundId) {
|
|
1145
|
+
throw new Error(`Pending outbound changed before auxiliary turn: ${outboundId}`);
|
|
1146
|
+
}
|
|
1147
|
+
const now = new Date().toISOString();
|
|
1148
|
+
const pending = {
|
|
1149
|
+
version: 1,
|
|
1150
|
+
outboundId,
|
|
1151
|
+
status: "waiting",
|
|
1152
|
+
conversationUrl: turn.conversationUrl
|
|
1153
|
+
?? nextState.conversationUrl
|
|
1154
|
+
?? null,
|
|
1155
|
+
conversationTargetId: turn.conversationTargetId
|
|
1156
|
+
?? nextState.conversationTargetId
|
|
1157
|
+
?? null,
|
|
1158
|
+
userMessageId: turn.userMessageId ?? null,
|
|
1159
|
+
userTurn: turn.userTurn ?? null,
|
|
1160
|
+
assistantBaseline: turn.assistantBaseline ?? null,
|
|
1161
|
+
createdAt: now,
|
|
1162
|
+
updatedAt: now,
|
|
1163
|
+
};
|
|
1164
|
+
nextState.pendingOutbound = null;
|
|
1165
|
+
nextState.pendingAuxiliaryTurn = pending;
|
|
1166
|
+
nextState.conversationUrl = pending.conversationUrl;
|
|
1167
|
+
nextState.conversationTargetId = pending.conversationTargetId;
|
|
1168
|
+
nextState.lastUserMessageId = pending.userMessageId;
|
|
1169
|
+
nextState.updatedAt = now;
|
|
1170
|
+
return structuredClone(pending);
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async completePendingAuxiliaryTurn({
|
|
1175
|
+
outboundId,
|
|
1176
|
+
assistantMessageId = null,
|
|
1177
|
+
assistantTurn = null,
|
|
1178
|
+
}) {
|
|
1179
|
+
return await this.#mutateState((nextState) => {
|
|
1180
|
+
const current = nextState.pendingAuxiliaryTurn;
|
|
1181
|
+
if (!current || current.outboundId !== outboundId) {
|
|
1182
|
+
throw new Error(`Pending auxiliary turn changed before completion: ${outboundId}`);
|
|
1183
|
+
}
|
|
1184
|
+
nextState.lastAssistantMessageId = assistantMessageId;
|
|
1185
|
+
nextState.pendingAuxiliaryTurn = null;
|
|
1186
|
+
nextState.updatedAt = new Date().toISOString();
|
|
1187
|
+
return {
|
|
1188
|
+
...structuredClone(current),
|
|
1189
|
+
status: "complete",
|
|
1190
|
+
assistantMessageId,
|
|
1191
|
+
assistantTurn,
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1133
1196
|
async refreshPendingAssistantTurn(handoffId, patch = {}) {
|
|
1134
1197
|
const requested = structuredClone(patch);
|
|
1135
1198
|
const protectedKeys = new Set([
|
|
@@ -1142,6 +1205,8 @@ export class AgentSession {
|
|
|
1142
1205
|
"createdAt",
|
|
1143
1206
|
"rawResponse",
|
|
1144
1207
|
"responseHash",
|
|
1208
|
+
"nativeImages",
|
|
1209
|
+
"nativeMusic",
|
|
1145
1210
|
]);
|
|
1146
1211
|
if (Object.keys(requested).some((key) => protectedKeys.has(key))) {
|
|
1147
1212
|
throw new Error("Assistant handoff refresh contains protected fields.");
|
|
@@ -1184,6 +1249,8 @@ export class AgentSession {
|
|
|
1184
1249
|
assistantMessageId = null,
|
|
1185
1250
|
assistantTurn = null,
|
|
1186
1251
|
rawResponse,
|
|
1252
|
+
nativeImages = [],
|
|
1253
|
+
nativeMusic = [],
|
|
1187
1254
|
completedAt = new Date().toISOString(),
|
|
1188
1255
|
}) {
|
|
1189
1256
|
if (typeof rawResponse !== "string") {
|
|
@@ -1197,6 +1264,8 @@ export class AgentSession {
|
|
|
1197
1264
|
assistantTurn,
|
|
1198
1265
|
rawResponse,
|
|
1199
1266
|
responseHash,
|
|
1267
|
+
nativeImages: structuredClone(nativeImages),
|
|
1268
|
+
nativeMusic: structuredClone(nativeMusic),
|
|
1200
1269
|
};
|
|
1201
1270
|
return await this.#mutateState((nextState) => {
|
|
1202
1271
|
const current = nextState.pendingAssistantTurn;
|
|
@@ -1211,6 +1280,8 @@ export class AgentSession {
|
|
|
1211
1280
|
assistantTurn: current.assistantTurn ?? null,
|
|
1212
1281
|
rawResponse: current.rawResponse,
|
|
1213
1282
|
responseHash: current.responseHash,
|
|
1283
|
+
nativeImages: current.nativeImages ?? [],
|
|
1284
|
+
nativeMusic: current.nativeMusic ?? [],
|
|
1214
1285
|
};
|
|
1215
1286
|
if (!isDeepStrictEqual(existing, completion)) {
|
|
1216
1287
|
throw new Error(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { resolveToolPath } from "../policy/path-guard.js";
|
|
4
|
+
|
|
5
|
+
const xmlStringArray = z.preprocess((value) => {
|
|
6
|
+
if (value == null || value === "") return [];
|
|
7
|
+
if (Array.isArray(value)) return value;
|
|
8
|
+
if (typeof value === "object" && value.item != null) {
|
|
9
|
+
return Array.isArray(value.item) ? value.item : [value.item];
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string" && value.trim().startsWith("[")) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(value);
|
|
14
|
+
if (Array.isArray(parsed)) return parsed;
|
|
15
|
+
} catch {
|
|
16
|
+
// Treat malformed JSON-looking input as one path; Zod will validate it.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [value];
|
|
20
|
+
}, z.array(z.string().min(1)));
|
|
21
|
+
|
|
22
|
+
export const imageGenerateInputSchema = z.object({
|
|
23
|
+
prompt: z.string().min(1),
|
|
24
|
+
output_path: z.string().min(1),
|
|
25
|
+
provider: z.enum(["auto", "chatgpt", "gemini", "grok"]).default("auto"),
|
|
26
|
+
aspect_ratio: z.enum(["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"])
|
|
27
|
+
.default("auto"),
|
|
28
|
+
reference_images: xmlStringArray.default([]),
|
|
29
|
+
timeout_ms: z.coerce.number().int().min(30_000).max(30 * 60_000)
|
|
30
|
+
.default(10 * 60_000),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export function registerImageTools(registry, { imageService }) {
|
|
34
|
+
if (!imageService) return registry;
|
|
35
|
+
registry.register({
|
|
36
|
+
name: "image.generate",
|
|
37
|
+
description: [
|
|
38
|
+
"Generate one raster image through a supported web provider and download it to the local project.",
|
|
39
|
+
"The operation succeeds only after a verified PNG, JPEG, or WebP artifact is atomically saved.",
|
|
40
|
+
"If the provider finishes with text instead of an image, its reply is returned as a failed image result; read that reply before deciding the next step.",
|
|
41
|
+
"Use provider=auto to use this session's provider when image-capable, otherwise ChatGPT;",
|
|
42
|
+
"or choose chatgpt, gemini, or grok explicitly.",
|
|
43
|
+
"Prefer aspect_ratio=auto. Unsupported ratios fall back to auto instead of failing.",
|
|
44
|
+
].join(" "),
|
|
45
|
+
inputDescription:
|
|
46
|
+
"<args><prompt><![CDATA[...]]></prompt><output_path>images/result.png</output_path><provider>auto</provider><aspect_ratio>auto|1:1|3:2|2:3|4:3|3:4|16:9|9:16</aspect_ratio><reference_images><item>references/style.png</item></reference_images><timeout_ms>600000</timeout_ms></args>",
|
|
47
|
+
risk: "write",
|
|
48
|
+
managesTimeout: true,
|
|
49
|
+
requiresVisibleBrowser: true,
|
|
50
|
+
inputSchema: imageGenerateInputSchema,
|
|
51
|
+
execute: async (args, context) => {
|
|
52
|
+
const referenceImages = [];
|
|
53
|
+
for (const rawPath of args.reference_images) {
|
|
54
|
+
const info = await resolveToolPath(context.projectRoot, rawPath);
|
|
55
|
+
if (!info.inside && !context.allowOutside) {
|
|
56
|
+
throw new Error(`Path is outside project root: ${info.path}`);
|
|
57
|
+
}
|
|
58
|
+
referenceImages.push(info.path);
|
|
59
|
+
}
|
|
60
|
+
const artifact = await imageService.generate({
|
|
61
|
+
prompt: args.prompt,
|
|
62
|
+
outputPath: args.output_path,
|
|
63
|
+
provider: args.provider,
|
|
64
|
+
aspectRatio: args.aspect_ratio,
|
|
65
|
+
referenceImages,
|
|
66
|
+
timeoutMs: args.timeout_ms,
|
|
67
|
+
}, context);
|
|
68
|
+
if (artifact.type === "text") {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
message: `Image provider returned text instead of an image:\n\n${artifact.text}`,
|
|
72
|
+
data: { response: artifact },
|
|
73
|
+
meta: { code: "IMAGE_TEXT_RESPONSE", completionUnknown: false },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const relative = path.relative(context.projectRoot, artifact.localPath);
|
|
77
|
+
const location = relative && !relative.startsWith("..")
|
|
78
|
+
? relative
|
|
79
|
+
: artifact.localPath;
|
|
80
|
+
let message = `Generated image at ${location}.`;
|
|
81
|
+
if (artifact.provenance?.requestedAspectRatio) {
|
|
82
|
+
message += ` Requested aspect_ratio ${artifact.provenance.requestedAspectRatio} is not supported by ${artifact.provenance.provider}; used ${artifact.provenance.aspectRatio}.`;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
message,
|
|
87
|
+
data: { artifact },
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return registry;
|
|
92
|
+
}
|
package/src/tools/registry.js
CHANGED
|
@@ -114,16 +114,28 @@ export class ToolRegistry {
|
|
|
114
114
|
meta: result.meta,
|
|
115
115
|
};
|
|
116
116
|
} catch (error) {
|
|
117
|
+
const meta = {
|
|
118
|
+
...(error?.meta ?? {}),
|
|
119
|
+
...(error?.code ? {
|
|
120
|
+
code: error.code,
|
|
121
|
+
recoverable: error.recoverable ?? false,
|
|
122
|
+
...(error.details ? { details: error.details } : {}),
|
|
123
|
+
} : {}),
|
|
124
|
+
...(error?.completionUnknown ? {
|
|
125
|
+
completionUnknown: true,
|
|
126
|
+
recoverable: error.recoverable ?? true,
|
|
127
|
+
} : {}),
|
|
128
|
+
};
|
|
117
129
|
return {
|
|
118
130
|
callId: preparedCall.id,
|
|
119
131
|
name: preparedCall.name,
|
|
120
132
|
ok: false,
|
|
121
133
|
message: error.message,
|
|
122
134
|
stderr: error.stack,
|
|
135
|
+
meta: Object.keys(meta).length ? meta : undefined,
|
|
123
136
|
};
|
|
124
137
|
} finally {
|
|
125
138
|
clearTimeout(timer);
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
|
-
|