codex-chatgpt-control 0.1.0-alpha.1

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 (179) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/contracts/v1/fixtures/backend-capabilities.json +54 -0
  5. package/contracts/v1/fixtures/backend-error-event-missing-stream-input.json +10 -0
  6. package/contracts/v1/fixtures/backend-error-missing-run-input.json +10 -0
  7. package/contracts/v1/fixtures/backend-runner-plan-request.json +12 -0
  8. package/contracts/v1/fixtures/backend-version.json +10 -0
  9. package/contracts/v1/fixtures/command-descriptors.json +803 -0
  10. package/contracts/v1/fixtures/describe-runner-run.json +31 -0
  11. package/contracts/v1/fixtures/doctor-bridge-upload.json +34 -0
  12. package/contracts/v1/fixtures/help-root.json +6 -0
  13. package/contracts/v1/fixtures/named-plan-two-turn.json +39 -0
  14. package/contracts/v1/fixtures/output-json-parse-success.json +115 -0
  15. package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +43 -0
  16. package/contracts/v1/fixtures/report-redaction-default.json +16 -0
  17. package/contracts/v1/fixtures/reports-create-redacted.json +16 -0
  18. package/contracts/v1/fixtures/reports-summarize-redacted.json +26 -0
  19. package/contracts/v1/fixtures/responses-basic-success.json +27 -0
  20. package/contracts/v1/fixtures/responses-hidden-instructions-unsupported.json +22 -0
  21. package/contracts/v1/fixtures/responses-unknown-field-unsupported.json +22 -0
  22. package/contracts/v1/fixtures/responses-unsupported-previous-response-id.json +22 -0
  23. package/contracts/v1/fixtures/responses-unsupported-temperature.json +22 -0
  24. package/contracts/v1/fixtures/run-basic-success.json +36 -0
  25. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +180 -0
  26. package/contracts/v1/fixtures/run-file-attach-success.json +63 -0
  27. package/contracts/v1/fixtures/run-report-redacted.json +52 -0
  28. package/contracts/v1/fixtures/run-selector-drift-blocker.json +60 -0
  29. package/contracts/v1/fixtures/run-timeout-partial.json +81 -0
  30. package/contracts/v1/fixtures/run-two-turn-success.json +83 -0
  31. package/contracts/v1/fixtures/run-upload-permission-blocker.json +56 -0
  32. package/contracts/v1/fixtures/runner-budget-blocker.json +106 -0
  33. package/contracts/v1/fixtures/runner-full-agent-config.json +50 -0
  34. package/contracts/v1/fixtures/runner-input-items-and-files-plan.json +53 -0
  35. package/contracts/v1/fixtures/runner-metadata-only-plan.json +31 -0
  36. package/contracts/v1/fixtures/runner-visible-prefix-plan.json +28 -0
  37. package/contracts/v1/fixtures/runner-visible-setup-plan.json +37 -0
  38. package/contracts/v1/fixtures/stream-basic.ndjson +3 -0
  39. package/contracts/v1/fixtures/stream-blocked.ndjson +2 -0
  40. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +3 -0
  41. package/contracts/v1/fixtures/workflow-ask-success.json +67 -0
  42. package/contracts/v1/manifest.json +210 -0
  43. package/contracts/v1/parity-suite.json +635 -0
  44. package/contracts/v1/schemas/agent.schema.json +52 -0
  45. package/contracts/v1/schemas/backend-event.schema.json +74 -0
  46. package/contracts/v1/schemas/backend-request.schema.json +59 -0
  47. package/contracts/v1/schemas/backend-response.schema.json +42 -0
  48. package/contracts/v1/schemas/capabilities.schema.json +33 -0
  49. package/contracts/v1/schemas/command-descriptor.schema.json +31 -0
  50. package/contracts/v1/schemas/command-result.schema.json +99 -0
  51. package/contracts/v1/schemas/manifest.schema.json +69 -0
  52. package/contracts/v1/schemas/response.schema.json +51 -0
  53. package/contracts/v1/schemas/run-result.schema.json +138 -0
  54. package/contracts/v1/schemas/sequence-plan.schema.json +44 -0
  55. package/contracts/v1/schemas/stream-event.schema.json +25 -0
  56. package/dist/codex-chatgpt-control-backend.mjs +5284 -0
  57. package/dist/codex-chatgpt-control.bundle.mjs +6013 -0
  58. package/dist/src/backend/client.d.ts +129 -0
  59. package/dist/src/backend/client.js +415 -0
  60. package/dist/src/backend/protocol.d.ts +74 -0
  61. package/dist/src/backend/protocol.js +128 -0
  62. package/dist/src/backend/session.d.ts +11 -0
  63. package/dist/src/backend/session.js +223 -0
  64. package/dist/src/backend/stdio-server.d.ts +9 -0
  65. package/dist/src/backend/stdio-server.js +88 -0
  66. package/dist/src/browser/attach.d.ts +8 -0
  67. package/dist/src/browser/attach.js +456 -0
  68. package/dist/src/browser/clipboard.d.ts +2 -0
  69. package/dist/src/browser/clipboard.js +26 -0
  70. package/dist/src/browser/downloads.d.ts +7 -0
  71. package/dist/src/browser/downloads.js +29 -0
  72. package/dist/src/browser/page-state.d.ts +17 -0
  73. package/dist/src/browser/page-state.js +66 -0
  74. package/dist/src/client.d.ts +167 -0
  75. package/dist/src/client.js +666 -0
  76. package/dist/src/commands/confirmations.d.ts +9 -0
  77. package/dist/src/commands/confirmations.js +33 -0
  78. package/dist/src/commands/context.d.ts +2 -0
  79. package/dist/src/commands/context.js +32 -0
  80. package/dist/src/commands/doctor.d.ts +16 -0
  81. package/dist/src/commands/doctor.js +120 -0
  82. package/dist/src/commands/files.d.ts +4 -0
  83. package/dist/src/commands/files.js +314 -0
  84. package/dist/src/commands/helpers.d.ts +16 -0
  85. package/dist/src/commands/helpers.js +128 -0
  86. package/dist/src/commands/messages.d.ts +16 -0
  87. package/dist/src/commands/messages.js +538 -0
  88. package/dist/src/commands/modes.d.ts +9 -0
  89. package/dist/src/commands/modes.js +323 -0
  90. package/dist/src/commands/output.d.ts +5 -0
  91. package/dist/src/commands/output.js +31 -0
  92. package/dist/src/commands/registry.d.ts +17 -0
  93. package/dist/src/commands/registry.js +275 -0
  94. package/dist/src/commands/reports.d.ts +14 -0
  95. package/dist/src/commands/reports.js +47 -0
  96. package/dist/src/commands/response-actions.d.ts +2 -0
  97. package/dist/src/commands/response-actions.js +141 -0
  98. package/dist/src/commands/sequence.d.ts +9 -0
  99. package/dist/src/commands/sequence.js +228 -0
  100. package/dist/src/commands/session.d.ts +2 -0
  101. package/dist/src/commands/session.js +25 -0
  102. package/dist/src/commands/threads.d.ts +6 -0
  103. package/dist/src/commands/threads.js +300 -0
  104. package/dist/src/dom/fixtures.d.ts +1 -0
  105. package/dist/src/dom/fixtures.js +8 -0
  106. package/dist/src/dom/menus.d.ts +8 -0
  107. package/dist/src/dom/menus.js +40 -0
  108. package/dist/src/dom/message-format.d.ts +30 -0
  109. package/dist/src/dom/message-format.js +593 -0
  110. package/dist/src/dom/messages.d.ts +40 -0
  111. package/dist/src/dom/messages.js +127 -0
  112. package/dist/src/dom/selectors.d.ts +20 -0
  113. package/dist/src/dom/selectors.js +72 -0
  114. package/dist/src/dom/visible-text.d.ts +5 -0
  115. package/dist/src/dom/visible-text.js +27 -0
  116. package/dist/src/errors.d.ts +32 -0
  117. package/dist/src/errors.js +127 -0
  118. package/dist/src/index.d.ts +34 -0
  119. package/dist/src/index.js +34 -0
  120. package/dist/src/logger.d.ts +15 -0
  121. package/dist/src/logger.js +25 -0
  122. package/dist/src/runner/agent.d.ts +2 -0
  123. package/dist/src/runner/agent.js +17 -0
  124. package/dist/src/runner/index.d.ts +7 -0
  125. package/dist/src/runner/index.js +7 -0
  126. package/dist/src/runner/interruptions.d.ts +3 -0
  127. package/dist/src/runner/interruptions.js +65 -0
  128. package/dist/src/runner/responses.d.ts +30 -0
  129. package/dist/src/runner/responses.js +171 -0
  130. package/dist/src/runner/result.d.ts +3 -0
  131. package/dist/src/runner/result.js +128 -0
  132. package/dist/src/runner/resume.d.ts +11 -0
  133. package/dist/src/runner/resume.js +26 -0
  134. package/dist/src/runner/stream.d.ts +13 -0
  135. package/dist/src/runner/stream.js +66 -0
  136. package/dist/src/runner/types.d.ts +264 -0
  137. package/dist/src/runner/types.js +1 -0
  138. package/dist/src/safety/blockers.d.ts +7 -0
  139. package/dist/src/safety/blockers.js +60 -0
  140. package/dist/src/safety/redaction.d.ts +2 -0
  141. package/dist/src/safety/redaction.js +18 -0
  142. package/dist/src/safety/report-redaction.d.ts +8 -0
  143. package/dist/src/safety/report-redaction.js +74 -0
  144. package/dist/src/safety/risk.d.ts +26 -0
  145. package/dist/src/safety/risk.js +28 -0
  146. package/dist/src/scripts/backend-server.d.ts +2 -0
  147. package/dist/src/scripts/backend-server.js +7 -0
  148. package/dist/src/scripts/check-parity-suite.d.ts +1 -0
  149. package/dist/src/scripts/check-parity-suite.js +10 -0
  150. package/dist/src/scripts/continue-thread.d.ts +27 -0
  151. package/dist/src/scripts/continue-thread.js +388 -0
  152. package/dist/src/scripts/live-smoke/harness.d.ts +11 -0
  153. package/dist/src/scripts/live-smoke/harness.js +151 -0
  154. package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
  155. package/dist/src/scripts/live-smoke/scenarios.js +648 -0
  156. package/dist/src/scripts/live-smoke/types.d.ts +53 -0
  157. package/dist/src/scripts/live-smoke/types.js +1 -0
  158. package/dist/src/scripts/live-smoke-module.d.ts +3 -0
  159. package/dist/src/scripts/live-smoke-module.js +2 -0
  160. package/dist/src/scripts/live-smoke.d.ts +1 -0
  161. package/dist/src/scripts/live-smoke.js +54 -0
  162. package/dist/src/scripts/smoke-attach-file.d.ts +1 -0
  163. package/dist/src/scripts/smoke-attach-file.js +39 -0
  164. package/dist/src/scripts/smoke-hi.d.ts +1 -0
  165. package/dist/src/scripts/smoke-hi.js +25 -0
  166. package/dist/src/scripts/smoke-search-open-read.d.ts +1 -0
  167. package/dist/src/scripts/smoke-search-open-read.js +41 -0
  168. package/dist/src/testing/parity-suite.d.ts +12 -0
  169. package/dist/src/testing/parity-suite.js +213 -0
  170. package/dist/src/types.d.ts +495 -0
  171. package/dist/src/types.js +1 -0
  172. package/package.json +81 -0
  173. package/references/agents-runner.md +32 -0
  174. package/references/backend-protocol.md +238 -0
  175. package/references/python-parity.md +168 -0
  176. package/references/responses-adapter.md +28 -0
  177. package/references/safety.md +14 -0
  178. package/references/streaming.md +15 -0
  179. package/references/troubleshooting.md +91 -0
@@ -0,0 +1,666 @@
1
+ import { attachFiles, downloadLatestFile } from "./commands/files.js";
2
+ import { doctor } from "./commands/doctor.js";
3
+ import { askMessage, composeMessage, readLatest, submitMessage, waitAndRead, waitForMessage } from "./commands/messages.js";
4
+ import { selectTool, setMode } from "./commands/modes.js";
5
+ import { createRunReport } from "./commands/reports.js";
6
+ import { copyResponse } from "./commands/response-actions.js";
7
+ import { commandDescriptors, describeCommand, helpText } from "./commands/registry.js";
8
+ import { runSequence } from "./commands/sequence.js";
9
+ import { bootstrap } from "./commands/session.js";
10
+ import { newThread, openThread, searchThreads } from "./commands/threads.js";
11
+ import { resultError, resultOk } from "./errors.js";
12
+ import { createChatGPTAgent } from "./runner/agent.js";
13
+ import { toRunResult } from "./runner/result.js";
14
+ import { responseFromRunResult, responsesCreateArgsToRunInput, unsupportedResponse, validateResponsesCreateArgs } from "./runner/responses.js";
15
+ import { streamFromRunResult } from "./runner/stream.js";
16
+ import { redactReportValue } from "./safety/report-redaction.js";
17
+ export function createChatGPT(options = {}) {
18
+ const env = runtimeEnv(options);
19
+ const limits = normalizeLimits(options.limits);
20
+ const runnerRun = ((agent, input, runnerOptions) => {
21
+ const run = () => runAgentWorkflow(agent, input, env, limits, options.defaults, options.reporting);
22
+ return runnerOptions?.stream === true ? streamFromRunResult(run) : run();
23
+ });
24
+ const runner = {
25
+ run: runnerRun,
26
+ plan: (agent, input) => planAgentWorkflow(agent, input, options.defaults)
27
+ };
28
+ return {
29
+ agent: config => createChatGPTAgent(config),
30
+ run: runner.run,
31
+ runner,
32
+ responses: {
33
+ create: args => createResponse(args, runner, env.now)
34
+ },
35
+ ask: args => runGuarded(planAskWorkflow(args, options.defaults), env, limits, reportOptions(args.report, options.reporting)),
36
+ askInThread: args => runGuarded(planAskWorkflow(args, options.defaults), env, limits, reportOptions(args.report, options.reporting)),
37
+ askWithFiles: args => runGuarded(planAskWorkflow(args, options.defaults), env, limits, reportOptions(args.report, options.reporting)),
38
+ askAndDownload: args => runGuarded(planAskWorkflow(args, options.defaults), env, limits, reportOptions(args.report, options.reporting)),
39
+ runMessages: args => runGuarded(planRunMessages(args, options.defaults), env, limits, reportOptions(args.report, options.reporting)),
40
+ openThread: thread => runSequence(planOpenThread(thread), env),
41
+ readLatest: args => readLatest(env, args),
42
+ copyLatest: args => copyResponse(env, args),
43
+ downloadLatest: args => downloadLatestFile(env, args),
44
+ runPlan: plan => runPlanInvocation(plan, env, limits, options.defaults, options.reporting),
45
+ doctor: args => doctor(env, args),
46
+ createReport: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
47
+ reports: {
48
+ create: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
49
+ redact: async (value, args) => resultOk(redactReportValue(value, args), {}),
50
+ summarize: async (result, args) => resultOk(redactReportValue(resultSummary(result), args), {})
51
+ },
52
+ plan: (name, args) => planByName(name, args, options.defaults),
53
+ commands: filter => commandDescriptors().filter(descriptor => filter?.layer === undefined || descriptor.layer === filter.layer),
54
+ describe: name => describeCommand(name),
55
+ help: topic => helpText(topic),
56
+ session: {
57
+ bootstrap: args => bootstrap(env, args)
58
+ },
59
+ threads: {
60
+ new: args => newThread(env, args),
61
+ search: args => searchThreads(env, args),
62
+ open: args => openThread(env, args)
63
+ },
64
+ messages: {
65
+ compose: args => composeMessage(env, args),
66
+ submit: args => submitMessage(env, args),
67
+ ask: args => askMessage(env, args),
68
+ wait: args => waitForMessage(env, args),
69
+ readLatest: args => readLatest(env, args),
70
+ waitAndRead: args => waitAndRead(env, args)
71
+ },
72
+ files: {
73
+ attach: args => attachFiles(env, args),
74
+ downloadLatest: args => downloadLatestFile(env, args)
75
+ },
76
+ modes: {
77
+ set: args => setMode(env, args)
78
+ },
79
+ tools: {
80
+ select: args => selectTool(env, args)
81
+ },
82
+ response: {
83
+ copy: args => copyResponse(env, args)
84
+ }
85
+ };
86
+ }
87
+ async function runGuarded(plan, env, limits, report) {
88
+ const budget = checkRunBudget(plan, limits);
89
+ if (budget !== undefined)
90
+ return budget;
91
+ const result = await runSequence(plan, env);
92
+ if (report === undefined || report.enabled === false)
93
+ return result;
94
+ const reportResult = await createRunReport(env, result, capReportOptions(report, limits));
95
+ if (reportResult.ok && reportResult.data !== undefined) {
96
+ if (reportResult.data.bytes > limits.maxReportBytesPerRun) {
97
+ const overBudget = {
98
+ ok: false,
99
+ status: "needs_confirmation",
100
+ warnings: [`Run report exceeded byte budget after creation: ${reportResult.data.bytes}/${limits.maxReportBytesPerRun}.`],
101
+ reportPath: reportResult.data.path,
102
+ blocker: {
103
+ kind: "confirmation",
104
+ code: "report_byte_budget_exceeded",
105
+ fieldPath: "limits.maxReportBytesPerRun",
106
+ message: `Workflow "${plan.name}" created a report larger than the configured budget (${reportResult.data.bytes}/${limits.maxReportBytesPerRun} bytes). Ask the user before preserving or sharing it.`,
107
+ remediation: [
108
+ {
109
+ label: "Confirm report retention",
110
+ instruction: "Ask the user whether to keep this report, increase maxReportBytesPerRun, or rerun with a smaller report preview.",
111
+ userActionRequired: true
112
+ }
113
+ ],
114
+ resumable: true
115
+ },
116
+ context: result.context
117
+ };
118
+ if (result.steps !== undefined)
119
+ overBudget.steps = result.steps;
120
+ return overBudget;
121
+ }
122
+ return {
123
+ ...result,
124
+ reportPath: reportResult.data.path,
125
+ warnings: [...result.warnings, ...reportResult.warnings]
126
+ };
127
+ }
128
+ return {
129
+ ...result,
130
+ warnings: [
131
+ ...result.warnings,
132
+ `Run report creation failed: ${reportResult.error?.message ?? reportResult.blocker?.message ?? reportResult.status}`
133
+ ]
134
+ };
135
+ }
136
+ function normalizeLimits(limits) {
137
+ return {
138
+ maxPromptsPerRun: limits?.maxPromptsPerRun ?? 5,
139
+ maxThreadsOpenedPerRun: limits?.maxThreadsOpenedPerRun ?? 3,
140
+ maxMessagesReadPerRun: limits?.maxMessagesReadPerRun ?? 10,
141
+ maxReportBytesPerRun: limits?.maxReportBytesPerRun ?? 2_000_000,
142
+ maxReportPreviewChars: limits?.maxReportPreviewChars ?? 240
143
+ };
144
+ }
145
+ function checkRunBudget(plan, limits) {
146
+ const prompts = plan.steps.filter(step => step.command === "messages.ask" || step.command === "messages.submit").length;
147
+ const threads = plan.steps.filter(step => step.command === "threads.new" || step.command === "threads.open").length;
148
+ const reads = plan.steps.filter(step => step.command === "messages.readLatest" || step.command === "messages.waitAndRead" || step.command === "response.copy").length
149
+ + plan.steps.filter(step => step.command === "messages.ask" && askStepReads(step.args)).length;
150
+ const violations = [];
151
+ if (prompts > limits.maxPromptsPerRun)
152
+ violations.push(`prompts ${prompts}/${limits.maxPromptsPerRun}`);
153
+ if (threads > limits.maxThreadsOpenedPerRun)
154
+ violations.push(`threads ${threads}/${limits.maxThreadsOpenedPerRun}`);
155
+ if (reads > limits.maxMessagesReadPerRun)
156
+ violations.push(`reads ${reads}/${limits.maxMessagesReadPerRun}`);
157
+ if (violations.length === 0)
158
+ return undefined;
159
+ return {
160
+ ok: false,
161
+ status: "needs_confirmation",
162
+ warnings: [],
163
+ blocker: {
164
+ kind: "confirmation",
165
+ code: "run_budget_exceeded",
166
+ fieldPath: "limits",
167
+ message: `Workflow "${plan.name}" exceeds ChatGPT browser-control run budget: ${violations.join(", ")}. Ask the user to confirm a bounded exception.`,
168
+ remediation: [
169
+ {
170
+ label: "Confirm bounded run",
171
+ instruction: "Ask the user to approve this specific over-budget run, or reduce the number of prompts, thread opens, or message reads.",
172
+ userActionRequired: true
173
+ }
174
+ ],
175
+ resumable: true
176
+ },
177
+ context: { timestamp: new Date().toISOString() }
178
+ };
179
+ }
180
+ function askStepReads(args) {
181
+ return args.read === true || typeof args.read === "object";
182
+ }
183
+ function reportOptions(request, defaults) {
184
+ if (request === false)
185
+ return undefined;
186
+ if (request === true)
187
+ return { ...(defaults ?? {}), enabled: true };
188
+ if (request !== undefined)
189
+ return { ...(defaults ?? {}), ...request, enabled: request.enabled ?? true };
190
+ return defaults?.enabled === true ? defaults : undefined;
191
+ }
192
+ function capReportOptions(report, limits) {
193
+ return {
194
+ ...report,
195
+ maxPreviewChars: Math.min(report.maxPreviewChars ?? limits.maxReportPreviewChars, limits.maxReportPreviewChars)
196
+ };
197
+ }
198
+ async function createResponse(args, runner, now) {
199
+ const validation = validateResponsesCreateArgs(args);
200
+ const timestamp = now?.() ?? new Date();
201
+ if (!validation.ok) {
202
+ return unsupportedResponse(validation.unsupported, timestamp);
203
+ }
204
+ const responseArgs = args;
205
+ const agentConfig = {
206
+ name: "responses-adapter",
207
+ instructionsMode: responseArgs.instructionsMode === "visible_prefix" ? "visible_prefix" : "metadata_only"
208
+ };
209
+ if (typeof responseArgs.instructions === "string") {
210
+ agentConfig.instructions = responseArgs.instructions;
211
+ }
212
+ const agent = createChatGPTAgent(agentConfig);
213
+ const result = await runner.run(agent, responsesCreateArgsToRunInput(responseArgs));
214
+ return responseFromRunResult(result, now?.() ?? timestamp);
215
+ }
216
+ async function runAgentWorkflow(agent, input, env, limits, defaults, reporting) {
217
+ try {
218
+ const normalized = normalizeRunnerInput(agent, input);
219
+ const plan = planAgentWorkflowFromNormalized(agent, normalized, defaults);
220
+ const report = reportOptions(normalized.report ?? agent.defaults.report, reporting);
221
+ const result = await runGuarded(plan, env, limits, report);
222
+ return toRunResult(agent, result);
223
+ }
224
+ catch (error) {
225
+ return toRunResult(agent, resultError(error instanceof Error ? error : new Error(String(error)), {}));
226
+ }
227
+ }
228
+ function planAgentWorkflow(agent, input, defaults = {}) {
229
+ return planAgentWorkflowFromNormalized(agent, normalizeRunnerInput(agent, input), defaults);
230
+ }
231
+ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
232
+ const wait = input.wait ?? agent.defaults.wait ?? defaults.wait ?? true;
233
+ const read = input.read ?? agent.defaults.read ?? defaults.read ?? { format: "markdown" };
234
+ const thread = input.thread ?? agent.defaults.thread ?? { type: "new" };
235
+ const steps = [
236
+ bootstrapStepForWorkflow(thread, input.existingTab ?? agent.defaults.existingTab ?? defaults.existingTab, input.preferExistingTab ?? agent.defaults.preferExistingTab ?? defaults.preferExistingTab),
237
+ ...threadSteps(thread)
238
+ ];
239
+ const mode = input.mode ?? agent.defaults.mode ?? defaults.mode;
240
+ if (mode !== undefined) {
241
+ steps.push({ id: "mode", command: "modes.set", args: mode });
242
+ }
243
+ for (const [index, tool] of input.tools.entries()) {
244
+ steps.push({ id: `tool${index + 1}`, command: "tools.select", args: tool });
245
+ }
246
+ if (input.files.length > 0) {
247
+ steps.push({ id: "attach", command: "files.attach", args: { paths: input.files } });
248
+ }
249
+ if (agent.instructionsMode === "visible_setup_message" && hasInstructions(agent)) {
250
+ steps.push({
251
+ id: "agent_setup",
252
+ command: "messages.ask",
253
+ args: {
254
+ text: renderAgentSetupMessage(agent),
255
+ wait,
256
+ read: false
257
+ }
258
+ });
259
+ }
260
+ steps.push({
261
+ id: "ask",
262
+ command: "messages.ask",
263
+ args: {
264
+ text: renderRunnerPrompt(agent, input.prompt),
265
+ wait,
266
+ read
267
+ }
268
+ });
269
+ if (input.copy !== undefined && input.copy !== false) {
270
+ steps.push({ id: "copy", command: "response.copy", args: input.copy });
271
+ }
272
+ if (input.download !== undefined && input.download !== false) {
273
+ steps.push({ id: "download", command: "files.downloadLatest", args: input.download });
274
+ }
275
+ return {
276
+ name: `agent-run:${agent.name}`,
277
+ policy: { stopOnError: true, returnPartial: true },
278
+ steps
279
+ };
280
+ }
281
+ function normalizeRunnerInput(agent, input) {
282
+ const args = typeof input === "string" ? { input } : input;
283
+ const collected = collectRunnerInput(args.input);
284
+ const attachments = normalizeRunnerAttachments(args.attachments);
285
+ const mode = args.mode;
286
+ const normalized = {
287
+ prompt: collected.prompt,
288
+ tools: args.tools ?? [],
289
+ files: [...collected.files, ...attachments]
290
+ };
291
+ if (args.thread !== undefined)
292
+ normalized.thread = args.thread;
293
+ if (args.existingTab !== undefined)
294
+ normalized.existingTab = args.existingTab;
295
+ if (args.preferExistingTab !== undefined)
296
+ normalized.preferExistingTab = args.preferExistingTab;
297
+ if (mode !== undefined)
298
+ normalized.mode = mode;
299
+ if (args.response !== undefined)
300
+ normalized.read = args.response;
301
+ if (args.download !== undefined)
302
+ normalized.download = args.download;
303
+ if (args.copy !== undefined)
304
+ normalized.copy = args.copy;
305
+ if (args.report !== undefined)
306
+ normalized.report = args.report;
307
+ if (normalized.prompt.trim().length === 0) {
308
+ throw new Error(`ChatGPT runner input for agent "${agent.name}" must include non-empty visible text.`);
309
+ }
310
+ return normalized;
311
+ }
312
+ function collectRunnerInput(input) {
313
+ if (typeof input === "string") {
314
+ return { prompt: input, files: [] };
315
+ }
316
+ const visibleInstructions = [];
317
+ const userText = [];
318
+ const files = [];
319
+ for (const item of input) {
320
+ switch (item.type) {
321
+ case "input_text":
322
+ userText.push(item.text);
323
+ break;
324
+ case "visible_instruction":
325
+ visibleInstructions.push(item.text);
326
+ break;
327
+ case "input_file":
328
+ files.push(item.path);
329
+ if (item.description !== undefined && item.description.trim().length > 0) {
330
+ userText.push(`Attached file context: ${item.description.trim()}`);
331
+ }
332
+ break;
333
+ }
334
+ }
335
+ const parts = [];
336
+ if (visibleInstructions.length > 0) {
337
+ parts.push(`<visible_instructions>\n${visibleInstructions.join("\n")}\n</visible_instructions>`);
338
+ }
339
+ if (userText.length > 0) {
340
+ parts.push(userText.join("\n\n"));
341
+ }
342
+ return { prompt: parts.join("\n\n"), files };
343
+ }
344
+ function normalizeRunnerAttachments(attachments) {
345
+ return (attachments ?? []).map(attachment => attachment.path);
346
+ }
347
+ function renderRunnerPrompt(agent, prompt) {
348
+ if (agent.instructionsMode !== "visible_prefix" || !hasInstructions(agent)) {
349
+ return prompt;
350
+ }
351
+ return `${renderAgentInstructionBlock(agent)}\n\n<user_request>\n${prompt}\n</user_request>`;
352
+ }
353
+ function renderAgentSetupMessage(agent) {
354
+ return `${renderAgentInstructionBlock(agent)}\n\nAcknowledge these visible setup instructions briefly, then wait for the next user request.`;
355
+ }
356
+ function renderAgentInstructionBlock(agent) {
357
+ return [
358
+ "<chatgpt_browser_agent>",
359
+ `Agent name: ${agent.name}`,
360
+ "Instructions:",
361
+ agent.instructions ?? "",
362
+ "</chatgpt_browser_agent>"
363
+ ].join("\n");
364
+ }
365
+ function hasInstructions(agent) {
366
+ return (agent.instructions ?? "").trim().length > 0;
367
+ }
368
+ async function runPlanInvocation(plan, env, limits, defaults, reporting) {
369
+ try {
370
+ if (!("steps" in plan) && plan.name === "doctor-upload") {
371
+ const result = await doctor(env, { check: ["bridge", "login", "upload"] });
372
+ return maybeAttachReport(env, result, reportOptions(plan.report, reporting), limits);
373
+ }
374
+ if (!("steps" in plan) && plan.name === "redacted-run-report") {
375
+ const input = isRecord(plan.input) ? plan.input : {};
376
+ const result = input.result;
377
+ if (!isCommandResult(result)) {
378
+ throw new Error('Named workflow "redacted-run-report" requires input.result to be a CommandResult.');
379
+ }
380
+ return createRunReport(env, result, capReportOptions(reportOptions(plan.report, reporting) ?? {}, limits));
381
+ }
382
+ const resolved = "steps" in plan ? plan : resolvePlan(plan, defaults);
383
+ return runGuarded(resolved, env, limits, reportOptions("report" in plan ? plan.report : undefined, reporting));
384
+ }
385
+ catch (error) {
386
+ return resultError(error instanceof Error ? error : new Error(String(error)), {});
387
+ }
388
+ }
389
+ async function maybeAttachReport(env, result, report, limits) {
390
+ if (report === undefined || report.enabled === false)
391
+ return result;
392
+ const reportResult = await createRunReport(env, result, capReportOptions(report, limits));
393
+ if (!reportResult.ok || reportResult.data === undefined)
394
+ return result;
395
+ return { ...result, reportPath: reportResult.data.path };
396
+ }
397
+ function runtimeEnv(options) {
398
+ const env = {};
399
+ if (options.agent !== undefined)
400
+ env.agent = options.agent;
401
+ if (options.browser !== undefined)
402
+ env.browser = options.browser;
403
+ if (options.page !== undefined)
404
+ env.page = options.page;
405
+ if (options.clipboard !== undefined)
406
+ env.clipboard = options.clipboard;
407
+ if (options.now !== undefined)
408
+ env.now = options.now;
409
+ return env;
410
+ }
411
+ function planAskWorkflow(args, defaults = {}) {
412
+ const thread = args.thread ?? { type: "new" };
413
+ const steps = [
414
+ bootstrapStepForWorkflow(thread, args.existingTab ?? defaults.existingTab, args.preferExistingTab ?? defaults.preferExistingTab),
415
+ ...threadSteps(thread)
416
+ ];
417
+ const mode = args.mode ?? defaults.mode;
418
+ if (mode !== undefined) {
419
+ steps.push({ id: "mode", command: "modes.set", args: mode });
420
+ }
421
+ for (const [index, tool] of (args.tools ?? []).entries()) {
422
+ steps.push({ id: `tool${index + 1}`, command: "tools.select", args: tool });
423
+ }
424
+ const files = normalizeFileInputs([...(args.files ?? []), ...(args.attachments ?? [])]);
425
+ if (files.length > 0) {
426
+ steps.push({ id: "attach", command: "files.attach", args: { paths: files } });
427
+ }
428
+ steps.push({
429
+ id: "ask",
430
+ command: "messages.ask",
431
+ args: {
432
+ text: args.prompt,
433
+ wait: args.wait ?? defaults.wait ?? true,
434
+ read: args.read ?? defaults.read ?? { format: "markdown" }
435
+ }
436
+ });
437
+ if (args.download !== undefined) {
438
+ steps.push({ id: "download", command: "files.downloadLatest", args: args.download });
439
+ }
440
+ return {
441
+ name: args.download === undefined ? "ask" : "ask-and-download",
442
+ policy: { stopOnError: true, returnPartial: true },
443
+ steps
444
+ };
445
+ }
446
+ function planRunMessages(args, defaults = {}) {
447
+ const thread = args.thread ?? { type: "new" };
448
+ const steps = [
449
+ bootstrapStepForWorkflow(thread, args.existingTab ?? defaults.existingTab, args.preferExistingTab ?? defaults.preferExistingTab),
450
+ ...threadSteps(thread)
451
+ ];
452
+ const mode = args.mode ?? defaults.mode;
453
+ if (mode !== undefined) {
454
+ steps.push({ id: "mode", command: "modes.set", args: mode });
455
+ }
456
+ args.messages.forEach((message, index) => {
457
+ steps.push({
458
+ id: message.id ?? `message${index + 1}`,
459
+ command: "messages.ask",
460
+ args: {
461
+ text: message.prompt,
462
+ wait: message.wait ?? defaults.wait ?? true,
463
+ read: message.read ?? defaults.read ?? { format: "markdown" }
464
+ }
465
+ });
466
+ });
467
+ return { name: "run-messages", policy: { stopOnError: true, returnPartial: true }, steps };
468
+ }
469
+ function planOpenThread(thread) {
470
+ return {
471
+ name: "open-thread",
472
+ policy: { stopOnError: true, returnPartial: true },
473
+ steps: [
474
+ { id: "bootstrap", command: "session.bootstrap" },
475
+ ...threadSteps(thread)
476
+ ]
477
+ };
478
+ }
479
+ function planByName(name, args, defaults = {}) {
480
+ const input = isRecord(args) ? args : {};
481
+ switch (name) {
482
+ case "new-ask-read":
483
+ return planAskWorkflow({ prompt: stringInput(input, "prompt"), thread: { type: "new" } }, defaults);
484
+ case "find-open-copy-latest":
485
+ return {
486
+ name,
487
+ steps: [
488
+ { id: "bootstrap", command: "session.bootstrap" },
489
+ { id: "find", command: "threads.search", args: { query: stringInput(input, "query"), limit: 5 } },
490
+ { id: "open", command: "threads.open", args: { fromStep: "find", select: "first" } },
491
+ { id: "copy", command: "response.copy", args: { which: "latest" } }
492
+ ]
493
+ };
494
+ case "find-open-ask-read":
495
+ return planAskWorkflow({
496
+ prompt: stringInput(input, "prompt"),
497
+ thread: { type: "search", query: stringInput(input, "query"), select: "first" }
498
+ }, defaults);
499
+ case "attach-ask-read":
500
+ return planAskWorkflow({
501
+ prompt: stringInput(input, "prompt"),
502
+ thread: { type: "new" },
503
+ files: arrayInput(input, "files").map(String)
504
+ }, defaults);
505
+ case "ask-and-download":
506
+ return planAskWorkflow({
507
+ prompt: stringInput(input, "prompt"),
508
+ thread: { type: "new" },
509
+ download: { destDir: stringInput(input, "destDir") }
510
+ }, defaults);
511
+ case "two-turn":
512
+ return planRunMessages({
513
+ thread: { type: "new" },
514
+ messages: [
515
+ { id: "first", prompt: stringInput(input, "first") },
516
+ { id: "second", prompt: stringInput(input, "second") }
517
+ ]
518
+ }, defaults);
519
+ default:
520
+ return undefined;
521
+ }
522
+ }
523
+ function resolvePlan(plan, defaults = {}) {
524
+ if ("steps" in plan)
525
+ return plan;
526
+ const resolved = planByName(plan.name, plan.input, defaults);
527
+ if (resolved === undefined) {
528
+ throw new Error(`Unknown ChatGPT workflow plan: ${plan.name}`);
529
+ }
530
+ return resolved;
531
+ }
532
+ function resultSummary(result) {
533
+ return {
534
+ ok: result.ok,
535
+ status: result.status,
536
+ warnings: result.warnings,
537
+ blocker: result.blocker,
538
+ error: result.error,
539
+ context: result.context,
540
+ reportPath: result.reportPath
541
+ };
542
+ }
543
+ function isCommandResult(value) {
544
+ return isRecord(value)
545
+ && typeof value.ok === "boolean"
546
+ && typeof value.status === "string"
547
+ && Array.isArray(value.warnings)
548
+ && isRecord(value.context)
549
+ && typeof value.context.timestamp === "string";
550
+ }
551
+ function bootstrapStepForWorkflow(thread, existingTab, preferExistingTab) {
552
+ const args = bootstrapArgsForWorkflow(thread, existingTab, preferExistingTab);
553
+ if (args === undefined) {
554
+ return { id: "bootstrap", command: "session.bootstrap" };
555
+ }
556
+ return { id: "bootstrap", command: "session.bootstrap", args };
557
+ }
558
+ function bootstrapArgsForWorkflow(thread, existingTab, preferExistingTab) {
559
+ const args = {};
560
+ if (existingTab !== undefined) {
561
+ args.existingTab = existingTab === true ? existingTabPolicyFromThread(thread) : existingTab;
562
+ }
563
+ if (preferExistingTab !== undefined) {
564
+ args.preferExistingTab = preferExistingTab;
565
+ }
566
+ return Object.keys(args).length === 0 ? undefined : args;
567
+ }
568
+ function existingTabPolicyFromThread(thread) {
569
+ const target = existingTabTargetFromThread(thread);
570
+ if (target === undefined) {
571
+ return {
572
+ target: { type: "selected", host: "chatgpt" },
573
+ ifMissing: "block",
574
+ ifMultiple: "first",
575
+ requireChatGPT: true
576
+ };
577
+ }
578
+ return {
579
+ target,
580
+ ifMissing: "block",
581
+ ifMultiple: target.type === "selected" ? "first" : "block",
582
+ requireChatGPT: true
583
+ };
584
+ }
585
+ function existingTabTargetFromThread(thread) {
586
+ if (isTypedThread(thread)) {
587
+ switch (thread.type) {
588
+ case "new":
589
+ case "search":
590
+ return undefined;
591
+ case "current":
592
+ return { type: "selected", host: "chatgpt" };
593
+ case "url":
594
+ return { type: "url", url: thread.url };
595
+ case "conversationId":
596
+ case "conversation_id":
597
+ return { type: "conversationId", conversationId: thread.conversationId };
598
+ case "title":
599
+ return { type: "title", title: thread.title, exact: false };
600
+ }
601
+ }
602
+ if (thread.url !== undefined)
603
+ return { type: "url", url: thread.url };
604
+ if (thread.conversationId !== undefined)
605
+ return { type: "conversationId", conversationId: thread.conversationId };
606
+ if (thread.title !== undefined)
607
+ return { type: "title", title: thread.title, exact: false };
608
+ return undefined;
609
+ }
610
+ function threadSteps(thread) {
611
+ if (isTypedThread(thread)) {
612
+ switch (thread.type) {
613
+ case "new":
614
+ return [{ id: "new", command: "threads.new" }];
615
+ case "current":
616
+ return [];
617
+ case "url":
618
+ return [{ id: "open", command: "threads.open", args: { url: thread.url } }];
619
+ case "conversationId":
620
+ return [{ id: "open", command: "threads.open", args: { conversationId: thread.conversationId } }];
621
+ case "conversation_id":
622
+ return [{ id: "open", command: "threads.open", args: { conversationId: thread.conversationId } }];
623
+ case "search":
624
+ return [
625
+ { id: "find", command: "threads.search", args: { query: thread.query, limit: thread.limit ?? 5 } },
626
+ { id: "open", command: "threads.open", args: { fromStep: "find", select: thread.select ?? "first" } }
627
+ ];
628
+ case "title":
629
+ return [{ id: "open", command: "threads.open", args: { title: thread.title } }];
630
+ }
631
+ }
632
+ if (thread.url !== undefined)
633
+ return [{ id: "open", command: "threads.open", args: { url: thread.url } }];
634
+ if (thread.conversationId !== undefined)
635
+ return [{ id: "open", command: "threads.open", args: { conversationId: thread.conversationId } }];
636
+ const query = thread.query ?? thread.title;
637
+ if (query === undefined)
638
+ return [];
639
+ return [
640
+ { id: "find", command: "threads.search", args: { query, limit: 5 } },
641
+ { id: "open", command: "threads.open", args: { fromStep: "find", select: thread.title === undefined ? "first" : { title: thread.title } } }
642
+ ];
643
+ }
644
+ function isTypedThread(thread) {
645
+ return "type" in thread;
646
+ }
647
+ function normalizeFileInputs(files) {
648
+ return files.map(file => typeof file === "string" ? file : file.path);
649
+ }
650
+ function isRecord(value) {
651
+ return value !== null && typeof value === "object" && !Array.isArray(value);
652
+ }
653
+ function stringInput(input, key) {
654
+ const value = input[key];
655
+ if (typeof value !== "string" || value.length === 0) {
656
+ throw new Error(`Named workflow input "${key}" must be a non-empty string.`);
657
+ }
658
+ return value;
659
+ }
660
+ function arrayInput(input, key) {
661
+ const value = input[key];
662
+ if (!Array.isArray(value)) {
663
+ throw new Error(`Named workflow input "${key}" must be an array.`);
664
+ }
665
+ return value;
666
+ }
@@ -0,0 +1,9 @@
1
+ import type { CommandResult } from "../types.js";
2
+ export type Confirmation = {
3
+ targetKind: string;
4
+ targetDisplayName: string;
5
+ action: string;
6
+ understood: true;
7
+ };
8
+ export declare function requireConfirmation<T>(confirm: Confirmation | undefined, expected: Omit<Confirmation, "understood">): CommandResult<T> | undefined;
9
+ export declare function rejectNetworkCommand<T>(command: string): CommandResult<T> | undefined;