task-while 0.0.2 → 0.0.3

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 (41) hide show
  1. package/README.md +32 -34
  2. package/package.json +2 -2
  3. package/src/adapters/fs/harness-store.ts +84 -0
  4. package/src/agents/claude.ts +159 -9
  5. package/src/agents/codex.ts +68 -4
  6. package/src/agents/event-log.ts +160 -15
  7. package/src/batch/discovery.ts +1 -1
  8. package/src/commands/batch.ts +63 -164
  9. package/src/commands/run-branch-helpers.ts +81 -0
  10. package/src/commands/run-providers.ts +77 -0
  11. package/src/commands/run.ts +121 -177
  12. package/src/core/create-runtime-ports.ts +118 -0
  13. package/src/core/runtime.ts +15 -36
  14. package/src/harness/in-memory-store.ts +45 -0
  15. package/src/harness/kernel.ts +226 -0
  16. package/src/harness/state.ts +47 -0
  17. package/src/harness/store.ts +26 -0
  18. package/src/harness/workflow-builders.ts +87 -0
  19. package/src/harness/workflow-program.ts +86 -0
  20. package/src/ports/agent.ts +17 -0
  21. package/src/ports/code-host.ts +23 -0
  22. package/src/programs/batch.ts +139 -0
  23. package/src/programs/run-direct.ts +209 -0
  24. package/src/programs/run-pr-transitions.ts +81 -0
  25. package/src/programs/run-pr.ts +290 -0
  26. package/src/programs/shared-steps.ts +252 -0
  27. package/src/schedulers/scheduler.ts +208 -0
  28. package/src/session/session.ts +127 -0
  29. package/src/workflow/config.ts +15 -0
  30. package/src/core/engine-helpers.ts +0 -114
  31. package/src/core/engine-outcomes.ts +0 -166
  32. package/src/core/engine.ts +0 -223
  33. package/src/core/orchestrator-helpers.ts +0 -52
  34. package/src/core/orchestrator-integrate-resume.ts +0 -149
  35. package/src/core/orchestrator-review-resume.ts +0 -228
  36. package/src/core/orchestrator-task-attempt.ts +0 -257
  37. package/src/core/orchestrator.ts +0 -99
  38. package/src/runtime/fs-runtime.ts +0 -209
  39. package/src/workflow/direct-preset.ts +0 -44
  40. package/src/workflow/preset.ts +0 -86
  41. package/src/workflow/pull-request-preset.ts +0 -312
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # task-while
2
2
 
3
- `task-while` is a git-first task orchestrator built around a task source protocol. The published package name and CLI binary are both `task-while`.
3
+ `task-while` is a git-first harness runtime built around a task source protocol. The published package name and CLI binary are both `task-while`.
4
4
 
5
5
  It reads workflow settings from `while.yaml`, opens the configured task source, executes one task at a time, reviews the result, integrates approved work, and creates one git commit per completed task. The built-in task sources are `spec-kit`, which consumes `spec.md`, `plan.md`, and `tasks.md` under `specs/<feature>/`, and `openspec`, which consumes an OpenSpec change under `openspec/changes/<change>/`.
6
6
 
7
- It also provides a standalone `batch` command for YAML-driven file processing that is independent from the feature/task orchestration workflow.
7
+ It also provides a standalone `batch` command for YAML-driven file processing that is independent from the feature/task harness runtime workflow.
8
8
 
9
9
  ## Requirements
10
10
 
11
- - Node.js 18 or newer
11
+ - Node.js 24 or newer
12
12
  - For `run`: a git repository with an initial commit
13
13
  - For `run`: a workspace with the directory layout required by the selected task source
14
14
  - For `run`: the files required by the selected task source
@@ -68,7 +68,7 @@ Current status:
68
68
  - `workflow.mode: pull-request` pushes a task branch, polls GitHub PR review from `chatgpt-codex-connector[bot]`, then squash-merges on approval
69
69
  - in `workflow.mode: pull-request`, reviewer `provider` still selects the remote reviewer, but any local reviewer `model` and `effort` values are ignored
70
70
  - `workflow.mode: pull-request` currently supports only `codex` as the remote reviewer provider
71
- - `task.maxIterations` applies globally to every task in the selected source session
71
+ - `task.maxIterations` uses the same configured limit for every task in the selected source session; run workflow retries share a single per-task budget across phases
72
72
 
73
73
  Example pull-request mode:
74
74
 
@@ -116,7 +116,7 @@ Useful flags:
116
116
  - `--feature <featureId>`: select the feature explicitly
117
117
  - For `task.source: openspec`, `--feature <featureId>` selects the OpenSpec change id
118
118
  - `--until-task <taskSelector>`: stop after the target task reaches `done`
119
- - `--verbose`: stream agent events to `stderr`
119
+ - `--verbose`: stream direct provider details to `stderr`, including Claude init/task/tool/result summaries and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
120
120
 
121
121
  ### `task-while batch`
122
122
 
@@ -160,14 +160,13 @@ Batch behavior:
160
160
  - batch `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
161
161
  - batch `claude` `effort` accepts `low`, `medium`, `high`, or `max`
162
162
  - each run scans files under the `batch.yaml` directory and filters them by `glob`
163
- - execution state is written beside the YAML file in `state.json`
164
163
  - structured results are written beside the YAML file in `results.json`
164
+ - internal harness state is written under `.while/harness/` beside the YAML file
165
165
  - result keys are relative to the directory that contains `batch.yaml`
166
- - `--verbose` streams provider agent events to `stderr` during batch execution
166
+ - `--verbose` streams direct provider details to `stderr` during batch execution, including Claude init/task/tool/result summaries and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
167
167
  - rerunning the command resumes unfinished work and skips files that already have accepted results
168
- - when the current `pending` queue is exhausted and `failed` is non-empty, the command persists a recycle transition that moves `failed` back into `pending` for the next round
169
- - the command exits only when both `pending` and `failed` are empty
170
- - there is no retry limit for file-level failures; failed files continue to be retried round by round
168
+ - failed files are suspended and retried after all pending files are processed
169
+ - file-level retries are limited by `maxRetries` (default 3); exhausted files are marked blocked
171
170
  - when `glob` matches no files, the command exits successfully without initializing a provider
172
171
 
173
172
  ## Task Lifecycle
@@ -261,52 +260,51 @@ task:
261
260
 
262
261
  ## What `task-while` Does Not Do
263
262
 
264
- `task-while` does not replace Spec Kit's project-level workflow. It does not run Spec Kit commands, checklists, hooks, or preset-installed skills.
263
+ `task-while` does not replace Spec Kit's project-level workflow. It does not run Spec Kit commands, checklists, or hooks.
265
264
 
266
265
  Its contract with the selected task source is simple:
267
266
 
268
267
  - the task source parses source artifacts and provides prompts plus completion operations
269
- - `task-while` orchestrates implement, review, integrate, and persistence around that protocol
268
+ - the harness runtime drives implement, review, integrate, and persistence around that protocol
270
269
 
271
270
  The standalone `batch` command is separate from this contract. It does not use task sources, task graphs, review/integrate stages, or git-first completion.
272
271
 
272
+ ## Architecture
273
+
274
+ `task-while` uses a state-machine control plane:
275
+
276
+ - **TaskState** per subject is the single source of truth, written atomically as JSON
277
+ - **Transition log** (append-only JSONL) records phase transitions for debugging
278
+ - **Artifacts** store large structured outputs (contracts, reviews, implementations) separately
279
+ - A **pure kernel interpreter** executes typed workflow programs (action/gate/branch nodes + declarative transition tables)
280
+ - A **session layer** drives multi-subject scheduling via pluggable schedulers
281
+ - All external effects flow through unified **ports** (AgentPort, CodeHostPort, GitPort)
282
+
273
283
  ## Runtime Layout
274
284
 
275
285
  `run` keeps runtime state under:
276
286
 
277
287
  ```text
278
- <source-entry>/<id>/.while/
288
+ <source-entry>/<id>/.while/harness/
289
+ state/<protocol>/<subject-id>.json — TaskState per subject (truth)
290
+ transitions/<protocol>/<subject-id>.jsonl — TransitionRecord log (debug)
291
+ artifacts/<protocol>/<subject-id>/*.json — Artifact per kind/iteration
279
292
  ```
280
293
 
281
- Important files:
282
-
283
- - `state.json`
284
- - `graph.json`
285
- - `report.json`
286
- - `events.jsonl`
287
- - `tasks/<taskHandle>/g<generation>/a<attempt>/implement.json`
288
- - `tasks/<taskHandle>/g<generation>/a<attempt>/review.json`
289
- - `tasks/<taskHandle>/g<generation>/a<attempt>/integrate.json`
290
-
291
- `.while` is runtime state, not the long-term source of truth. Pull-request review recovery reloads persisted `implement` artifacts by `taskHandle`, `generation`, and `attempt`.
294
+ `.while` is runtime state, not the long-term source of truth. Resume reads the state file directly — no event replay needed.
292
295
 
293
296
  `batch` keeps runtime files beside the YAML config:
294
297
 
295
298
  ```text
296
299
  <config-dir>/
297
300
  ├── batch.yaml
298
- ├── state.json
299
- └── results.json
301
+ ├── results.json
302
+ └── .while/harness/
303
+ ├── state/batch/*.json
304
+ ├── transitions/batch/*.jsonl
305
+ └── artifacts/batch/...
300
306
  ```
301
307
 
302
- `state.json` contains:
303
-
304
- - `pending`
305
- - `inProgress`
306
- - `failed`
307
-
308
- `failed` is the current round's failure buffer. When `pending` becomes empty, those paths are persisted back into `pending` and retried in the next round. Historical state entries whose files no longer exist are dropped when a new run starts.
309
-
310
308
  `results.json` maps accepted structured output by file path relative to the `batch.yaml` directory. If the config lives under a subdirectory and uses patterns such as `../input/*.txt`, the keys keep that relative form.
311
309
 
312
310
  ## Publishing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-while",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "packageManager": "pnpm@10.32.1",
5
5
  "description": "Git-first task orchestrator for task-source workspaces",
6
6
  "author": "Zhang Yu",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@anthropic-ai/claude-agent-sdk": "^0.2.92",
51
- "@openai/codex-sdk": "^0.116.0",
51
+ "@openai/codex-sdk": "^0.118.0",
52
52
  "ajv": "^8.18.0",
53
53
  "arg": "^5.0.2",
54
54
  "execa": "^8.0.1",
@@ -0,0 +1,84 @@
1
+ import { appendFile, mkdir, readdir, rename, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { pathExists, readJson } from 'fs-extra'
5
+
6
+ import type { HarnessStore } from '../../harness/store'
7
+
8
+ function encodeArtifactFileName(artifactId: string) {
9
+ return encodeURIComponent(artifactId)
10
+ }
11
+
12
+ export function createFsHarnessStore(root: string): HarnessStore {
13
+ const stateFile = (protocol: string, subjectId: string) =>
14
+ path.join(root, 'state', protocol, `${encodeURIComponent(subjectId)}.json`)
15
+
16
+ const artifactFile = (
17
+ protocol: string,
18
+ subjectId: string,
19
+ artifactId: string,
20
+ ) =>
21
+ path.join(
22
+ root,
23
+ 'artifacts',
24
+ protocol,
25
+ encodeURIComponent(subjectId),
26
+ `${encodeArtifactFileName(artifactId)}.json`,
27
+ )
28
+
29
+ const artifactDir = (protocol: string, subjectId: string) =>
30
+ path.join(root, 'artifacts', protocol, encodeURIComponent(subjectId))
31
+
32
+ const transitionFile = (protocol: string, subjectId: string) =>
33
+ path.join(
34
+ root,
35
+ 'transitions',
36
+ protocol,
37
+ `${encodeURIComponent(subjectId)}.jsonl`,
38
+ )
39
+
40
+ return {
41
+ async appendTransition(protocol, subjectId, record) {
42
+ const file = transitionFile(protocol, subjectId)
43
+ await mkdir(path.dirname(file), { recursive: true })
44
+ await appendFile(file, `${JSON.stringify(record)}\n`)
45
+ },
46
+ async listArtifacts(protocol, subjectId) {
47
+ const dir = artifactDir(protocol, subjectId)
48
+ if (!(await pathExists(dir))) {
49
+ return []
50
+ }
51
+ const directoryEntries = await readdir(dir)
52
+ const entries = directoryEntries.filter((e) => e.endsWith('.json'))
53
+ return Promise.all(
54
+ entries.map((entry) => readJson(path.join(dir, entry))),
55
+ )
56
+ },
57
+ async loadArtifact(protocol, subjectId, artifactId) {
58
+ const file = artifactFile(protocol, subjectId, artifactId)
59
+ if (!(await pathExists(file))) {
60
+ return null
61
+ }
62
+ return readJson(file)
63
+ },
64
+ async loadState(protocol, subjectId) {
65
+ const file = stateFile(protocol, subjectId)
66
+ if (!(await pathExists(file))) {
67
+ return null
68
+ }
69
+ return readJson(file)
70
+ },
71
+ async saveArtifact(protocol, subjectId, artifact) {
72
+ const file = artifactFile(protocol, subjectId, artifact.id)
73
+ await mkdir(path.dirname(file), { recursive: true })
74
+ await writeFile(file, JSON.stringify(artifact, null, 2))
75
+ },
76
+ async saveState(protocol, subjectId, state) {
77
+ const file = stateFile(protocol, subjectId)
78
+ const tmpFile = `${file}.tmp`
79
+ await mkdir(path.dirname(file), { recursive: true })
80
+ await writeFile(tmpFile, JSON.stringify(state, null, 2))
81
+ await rename(tmpFile, file)
82
+ },
83
+ }
84
+ }
@@ -22,11 +22,45 @@ export interface ClaudeTextEvent {
22
22
  type: 'text'
23
23
  }
24
24
 
25
- export interface ClaudeAssistantEvent {
26
- type: 'assistant'
25
+ export interface ClaudeInitEvent {
26
+ mcpServers: { name: string; status: string }[]
27
+ model: string
28
+ permissionMode: string
29
+ skills: string[]
30
+ tools: string[]
31
+ type: 'system.init'
32
+ }
33
+
34
+ export interface ClaudeTaskStartedEvent {
35
+ description: string
36
+ taskId: string
37
+ type: 'task.started'
38
+ }
39
+
40
+ export interface ClaudeTaskProgressEvent {
41
+ description: string
42
+ lastToolName?: string
43
+ summary?: string
44
+ taskId: string
45
+ type: 'task.progress'
46
+ }
47
+
48
+ export interface ClaudeToolProgressEvent {
49
+ elapsedTimeSeconds: number
50
+ toolName: string
51
+ toolUseId: string
52
+ type: 'tool.progress'
53
+ }
54
+
55
+ export interface ClaudeToolSummaryEvent {
56
+ summary: string
57
+ type: 'tool.summary'
27
58
  }
28
59
 
29
60
  export interface ClaudeResultEvent {
61
+ durationMs: number
62
+ numTurns: number
63
+ subtype: 'success'
30
64
  type: 'result'
31
65
  }
32
66
 
@@ -36,15 +70,21 @@ export interface ClaudeErrorEvent {
36
70
  }
37
71
 
38
72
  export type ClaudeAgentEvent =
39
- | ClaudeAssistantEvent
40
73
  | ClaudeErrorEvent
74
+ | ClaudeInitEvent
41
75
  | ClaudeResultEvent
76
+ | ClaudeTaskProgressEvent
77
+ | ClaudeTaskStartedEvent
42
78
  | ClaudeTextEvent
79
+ | ClaudeToolProgressEvent
80
+ | ClaudeToolSummaryEvent
43
81
 
44
82
  export type ClaudeAgentEventHandler = (event: ClaudeAgentEvent) => void
45
83
 
46
84
  interface QueryResultMessage {
85
+ duration_ms?: number
47
86
  errors?: string[]
87
+ num_turns?: number
48
88
  structured_output?: unknown
49
89
  subtype: string
50
90
  type: 'result'
@@ -62,10 +102,53 @@ interface QueryAssistantMessage {
62
102
  type: 'assistant'
63
103
  }
64
104
 
105
+ interface QuerySystemInitMessage {
106
+ mcp_servers: { name: string; status: string }[]
107
+ model: string
108
+ permissionMode: string
109
+ skills: string[]
110
+ subtype: 'init'
111
+ tools: string[]
112
+ type: 'system'
113
+ }
114
+
115
+ interface QueryTaskStartedMessage {
116
+ description: string
117
+ subtype: 'task_started'
118
+ task_id: string
119
+ type: 'system'
120
+ }
121
+
122
+ interface QueryTaskProgressMessage {
123
+ description: string
124
+ last_tool_name?: string
125
+ subtype: 'task_progress'
126
+ summary?: string
127
+ task_id: string
128
+ type: 'system'
129
+ }
130
+
131
+ interface QueryToolProgressMessage {
132
+ elapsed_time_seconds: number
133
+ tool_name: string
134
+ tool_use_id: string
135
+ type: 'tool_progress'
136
+ }
137
+
138
+ interface QueryToolUseSummaryMessage {
139
+ summary: string
140
+ type: 'tool_use_summary'
141
+ }
142
+
65
143
  type QueryMessage =
66
144
  | QueryAssistantMessage
67
145
  | QueryResultMessage
68
146
  | QueryStreamEventMessage
147
+ | QuerySystemInitMessage
148
+ | QueryTaskProgressMessage
149
+ | QueryTaskStartedMessage
150
+ | QueryToolProgressMessage
151
+ | QueryToolUseSummaryMessage
69
152
 
70
153
  export interface ClaudeAgentClientOptions extends ClaudeProviderOptions {
71
154
  onEvent?: ClaudeAgentEventHandler
@@ -90,6 +173,65 @@ export class ClaudeAgentClient
90
173
  let structuredOutput: unknown = null
91
174
 
92
175
  for await (const message of messages) {
176
+ if (
177
+ message.type === 'system' &&
178
+ message.subtype === 'init' &&
179
+ this.options.onEvent
180
+ ) {
181
+ this.options.onEvent({
182
+ mcpServers: message.mcp_servers,
183
+ model: message.model,
184
+ permissionMode: message.permissionMode,
185
+ skills: message.skills,
186
+ tools: message.tools,
187
+ type: 'system.init',
188
+ })
189
+ }
190
+
191
+ if (
192
+ message.type === 'system' &&
193
+ message.subtype === 'task_started' &&
194
+ this.options.onEvent
195
+ ) {
196
+ this.options.onEvent({
197
+ description: message.description,
198
+ taskId: message.task_id,
199
+ type: 'task.started',
200
+ })
201
+ }
202
+
203
+ if (
204
+ message.type === 'system' &&
205
+ message.subtype === 'task_progress' &&
206
+ this.options.onEvent
207
+ ) {
208
+ this.options.onEvent({
209
+ description: message.description,
210
+ taskId: message.task_id,
211
+ type: 'task.progress',
212
+ ...(message.last_tool_name
213
+ ? { lastToolName: message.last_tool_name }
214
+ : {}),
215
+ ...(message.summary ? { summary: message.summary } : {}),
216
+ })
217
+ }
218
+
219
+ if (message.type === 'tool_progress' && this.options.onEvent) {
220
+ this.options.onEvent({
221
+ elapsedTimeSeconds: message.elapsed_time_seconds,
222
+ toolName: message.tool_name,
223
+ toolUseId: message.tool_use_id,
224
+ type: 'tool.progress',
225
+ })
226
+ }
227
+
228
+ if (message.type === 'tool_use_summary' && this.options.onEvent) {
229
+ this.options.onEvent({
230
+ summary: message.summary,
231
+ type: 'tool.summary',
232
+ })
233
+ }
234
+
93
235
  if (message.type === 'stream_event' && this.options.onEvent) {
94
236
  const event = message.event
95
237
  if (
@@ -101,10 +243,6 @@ export class ClaudeAgentClient
101
243
  }
102
244
  }
103
245
 
104
- if (message.type === 'assistant' && this.options.onEvent) {
105
- this.options.onEvent({ type: 'assistant' })
106
- }
107
-
108
246
  if (message.type === 'result') {
109
247
  if (message.subtype !== 'success') {
110
248
  const detail = message.errors?.join('; ') ?? message.subtype
@@ -112,7 +250,12 @@ export class ClaudeAgentClient
112
250
  }
113
251
  structuredOutput = message.structured_output ?? null
114
252
  if (this.options.onEvent) {
115
- this.options.onEvent({ type: 'result' })
253
+ this.options.onEvent({
254
+ durationMs: message.duration_ms ?? 0,
255
+ numTurns: message.num_turns ?? 0,
256
+ subtype: 'success',
257
+ type: 'result',
258
+ })
116
259
  }
117
260
  }
118
261
  }
@@ -138,12 +281,19 @@ export class ClaudeAgentClient
138
281
  const queryOptions = {
139
282
  allowDangerouslySkipPermissions: true,
140
283
  cwd: this.options.workspaceRoot,
141
- includePartialMessages: !!this.options.onEvent,
142
284
  permissionMode: 'bypassPermissions',
143
285
  outputFormat: {
144
286
  schema: input.outputSchema,
145
287
  type: 'json_schema',
146
288
  },
289
+ ...(this.options.onEvent
290
+ ? {
291
+ agentProgressSummaries: true,
292
+ includePartialMessages: true,
293
+ }
294
+ : {
295
+ includePartialMessages: false,
296
+ }),
147
297
  ...(this.options.model ? { model: this.options.model } : {}),
148
298
  ...(this.options.effort ? { effort: this.options.effort } : {}),
149
299
  } satisfies ClaudeQueryOptions
@@ -58,11 +58,75 @@ export interface CodexTurnFailedError {
58
58
  message: string
59
59
  }
60
60
 
61
- export interface CodexItemPayload {
62
- text?: string
63
- type: string
61
+ export interface CodexAgentMessageItem {
62
+ id: string
63
+ text: string
64
+ type: 'agent_message'
64
65
  }
65
66
 
67
+ export interface CodexReasoningItem {
68
+ id: string
69
+ text: string
70
+ type: 'reasoning'
71
+ }
72
+
73
+ export interface CodexCommandExecutionItem {
74
+ aggregated_output: string
75
+ command: string
76
+ exit_code?: number
77
+ id: string
78
+ status: 'completed' | 'failed' | 'in_progress'
79
+ type: 'command_execution'
80
+ }
81
+
82
+ export interface CodexFileChangeItem {
83
+ changes: { kind: 'add' | 'delete' | 'update'; path: string }[]
84
+ id: string
85
+ status: 'completed' | 'failed'
86
+ type: 'file_change'
87
+ }
88
+
89
+ export interface CodexMcpToolCallItem {
90
+ arguments: unknown
91
+ error?: { message: string }
92
+ id: string
93
+ result?: {
94
+ structured_content: unknown
95
+ }
96
+ server: string
97
+ status: 'completed' | 'failed' | 'in_progress'
98
+ tool: string
99
+ type: 'mcp_tool_call'
100
+ }
101
+
102
+ export interface CodexWebSearchItem {
103
+ id: string
104
+ query: string
105
+ type: 'web_search'
106
+ }
107
+
108
+ export interface CodexTodoListItem {
109
+ id: string
110
+ items: { completed: boolean; text: string }[]
111
+ type: 'todo_list'
112
+ }
113
+
114
+ export interface CodexErrorItem {
115
+ id: string
116
+ message: string
117
+ type: 'error'
118
+ }
119
+
120
+ export type CodexItemPayload =
121
+ | CodexAgentMessageItem
122
+ | CodexCommandExecutionItem
123
+ | CodexErrorItem
124
+ | CodexFileChangeItem
125
+ | CodexMcpToolCallItem
126
+ | CodexReasoningItem
127
+ | CodexTodoListItem
128
+ | CodexWebSearchItem
129
+
66
130
  export type CodexThreadEvent =
67
131
  | CodexErrorEvent
68
132
  | CodexItemEvent
@@ -147,7 +211,7 @@ export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
147
211
  event.type === 'item.completed' &&
148
212
  event.item.type === 'agent_message'
149
213
  ) {
150
- finalResponse = event.item.text?.trim() ?? ''
214
+ finalResponse = event.item.text.trim()
151
215
  }
152
216
  }
153
217