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.
Files changed (34) hide show
  1. package/README.md +161 -7
  2. package/package.json +5 -4
  3. package/src/artifacts/artifact-store.js +154 -0
  4. package/src/audio/music-artifact.js +66 -0
  5. package/src/audio/native-music-receiver.js +91 -0
  6. package/src/browser/base-web-adapter.js +354 -50
  7. package/src/browser/chatgpt-dom.js +179 -0
  8. package/src/browser/chatgpt-web-adapter.js +89 -125
  9. package/src/browser/fake-web-model-adapter.js +16 -1
  10. package/src/browser/gemini-web-adapter.js +61 -20
  11. package/src/browser/grok-web-adapter.js +35 -3
  12. package/src/browser/rendered-text.js +11 -0
  13. package/src/cli/i18n.js +40 -10
  14. package/src/cli/main.js +32 -3
  15. package/src/cli/prompt-input.js +68 -0
  16. package/src/image/browser-image-driver.js +238 -0
  17. package/src/image/browser-image-session-pool.js +96 -0
  18. package/src/image/image-generation-service.js +101 -0
  19. package/src/image/image-provider-driver.js +22 -0
  20. package/src/image/native-image-receiver.js +66 -0
  21. package/src/image/provider-registry.js +23 -0
  22. package/src/image/providers/chatgpt-image-driver.js +211 -0
  23. package/src/image/providers/gemini-image-driver.js +100 -0
  24. package/src/image/providers/grok-image-driver.js +193 -0
  25. package/src/policy/policy-engine.js +10 -5
  26. package/src/protocol/envelope.js +74 -0
  27. package/src/protocol/markers.js +16 -0
  28. package/src/protocol/prompt-builder.js +12 -9
  29. package/src/protocol/xml-protocol.js +36 -18
  30. package/src/runtime/agent-runtime.js +345 -65
  31. package/src/session/agent-session.js +73 -2
  32. package/src/tools/image-tools.js +92 -0
  33. package/src/tools/registry.js +13 -1
  34. 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
- if (error.code !== "EADDRINUSE") {
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
+ }
@@ -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
-