@flowdesk/opencode-plugin 0.1.13 → 0.1.14
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 +1 -1
- package/dist/agent-task-output.d.ts +17 -0
- package/dist/agent-task-output.d.ts.map +1 -0
- package/dist/agent-task-output.js +119 -0
- package/dist/agent-task-output.js.map +1 -0
- package/dist/agent-task-runner.d.ts +23 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +410 -81
- package/dist/agent-task-runner.js.map +1 -1
- package/dist/auto-continue-preview-tool.d.ts +36 -0
- package/dist/auto-continue-preview-tool.d.ts.map +1 -0
- package/dist/auto-continue-preview-tool.js +119 -0
- package/dist/auto-continue-preview-tool.js.map +1 -0
- package/dist/completion-ui-cache.d.ts +6 -0
- package/dist/completion-ui-cache.d.ts.map +1 -0
- package/dist/completion-ui-cache.js +260 -0
- package/dist/completion-ui-cache.js.map +1 -0
- package/dist/event-hook-observer.d.ts +14 -0
- package/dist/event-hook-observer.d.ts.map +1 -0
- package/dist/event-hook-observer.js +193 -0
- package/dist/event-hook-observer.js.map +1 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +7 -3
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/model-selection-engine.d.ts +47 -0
- package/dist/model-selection-engine.d.ts.map +1 -0
- package/dist/model-selection-engine.js +175 -0
- package/dist/model-selection-engine.js.map +1 -0
- package/dist/provider-usage-live-tool.d.ts +10 -0
- package/dist/provider-usage-live-tool.d.ts.map +1 -1
- package/dist/provider-usage-live-tool.js +145 -18
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/server.d.ts +35 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +447 -19
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +33 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +459 -2
- package/dist/stall-recovery.js.map +1 -1
- package/dist/status-live-tool.d.ts +54 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +448 -44
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-subtask-activity.d.ts +69 -0
- package/dist/tui-subtask-activity.d.ts.map +1 -0
- package/dist/tui-subtask-activity.js +266 -0
- package/dist/tui-subtask-activity.js.map +1 -0
- package/dist/tui-usage-snapshot.d.ts +14 -0
- package/dist/tui-usage-snapshot.d.ts.map +1 -1
- package/dist/tui-usage-snapshot.js +189 -8
- package/dist/tui-usage-snapshot.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +72 -41
- package/dist/tui.js.map +1 -1
- package/dist/workflow-assign-tool.d.ts +23 -0
- package/dist/workflow-assign-tool.d.ts.map +1 -0
- package/dist/workflow-assign-tool.js +117 -0
- package/dist/workflow-assign-tool.js.map +1 -0
- package/dist/workflow-author-tool.d.ts +29 -0
- package/dist/workflow-author-tool.d.ts.map +1 -0
- package/dist/workflow-author-tool.js +227 -0
- package/dist/workflow-author-tool.js.map +1 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -1
- package/dist/workflow-dispatch-tool.js +32 -2
- package/dist/workflow-dispatch-tool.js.map +1 -1
- package/dist/workflow-orchestrator.d.ts +31 -0
- package/dist/workflow-orchestrator.d.ts.map +1 -0
- package/dist/workflow-orchestrator.js +160 -0
- package/dist/workflow-orchestrator.js.map +1 -0
- package/dist/workflow-scheduler.d.ts.map +1 -1
- package/dist/workflow-scheduler.js +3 -1
- package/dist/workflow-scheduler.js.map +1 -1
- package/dist/workflow-synthesis-tool.d.ts +31 -0
- package/dist/workflow-synthesis-tool.d.ts.map +1 -0
- package/dist/workflow-synthesis-tool.js +194 -0
- package/dist/workflow-synthesis-tool.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, prepareFlowDeskSessionEvidenceWriteIntentV1, } from "@flowdesk/core";
|
|
3
3
|
import { launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1, materializeFlowDeskRuntimeLaneLaunchLifecycleEvidenceV1, } from "./managed-dispatch-adapter.js";
|
|
4
|
+
import { observeFlowDeskAgentTaskOutputV1 } from "./agent-task-output.js";
|
|
5
|
+
import { refreshFlowDeskCompletionUiCachesV1 } from "./completion-ui-cache.js";
|
|
4
6
|
import { recordFlowDeskLaneHeartbeatV1 } from "./lane-heartbeat-writer.js";
|
|
5
7
|
const TASK_RESULT_MAX_TEXT = 32_768;
|
|
6
8
|
const AGENT_TASK_CONTEXT_MAX_PROMPT_TEXT = 32_768;
|
|
9
|
+
const INVALID_PARENT_SESSION_REF = "ses-invalid-parent-session-binding";
|
|
10
|
+
/** Schema version for async child session tracking evidence */
|
|
11
|
+
export const AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION = "flowdesk.agent_task_child_session.v1";
|
|
12
|
+
export function sanitizeFlowDeskTaskResultTextV1(text) {
|
|
13
|
+
return {
|
|
14
|
+
text: text.length > TASK_RESULT_MAX_TEXT ? text.slice(0, TASK_RESULT_MAX_TEXT) : text,
|
|
15
|
+
changed: false,
|
|
16
|
+
truncated: text.length > TASK_RESULT_MAX_TEXT,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
7
19
|
function agentTaskLaunchPlan(input) {
|
|
8
20
|
return {
|
|
9
21
|
schema_version: "flowdesk.runtime_lane_launch_plan.v1",
|
|
@@ -32,66 +44,162 @@ function agentTaskLaunchPlan(input) {
|
|
|
32
44
|
runtimeExecution: false,
|
|
33
45
|
};
|
|
34
46
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
47
|
+
function validateAgentTaskParentSessionId(parentSessionId) {
|
|
48
|
+
const value = parentSessionId.trim();
|
|
49
|
+
if (value.length === 0)
|
|
50
|
+
return { ok: false, redactedReason: "missing_parent_session_binding", parentSessionRef: INVALID_PARENT_SESSION_REF };
|
|
51
|
+
if (value.length > 128)
|
|
52
|
+
return { ok: false, redactedReason: "invalid_parent_session_binding", parentSessionRef: INVALID_PARENT_SESSION_REF };
|
|
53
|
+
// `ses-...` is FlowDesk's opaque session-ref wrapper, not the raw OpenCode
|
|
54
|
+
// session id expected by SDK `session.create({ parentID })`. Accepting it here
|
|
55
|
+
// causes evidence such as `ses-ses-flowdesk-coordinator` and can make the SDK
|
|
56
|
+
// wait on a non-existent synthetic parent session until launch timeout.
|
|
57
|
+
if (value.startsWith("ses-"))
|
|
58
|
+
return { ok: false, redactedReason: "invalid_parent_session_binding", parentSessionRef: INVALID_PARENT_SESSION_REF };
|
|
59
|
+
if (/\s/.test(value))
|
|
60
|
+
return { ok: false, redactedReason: "invalid_parent_session_binding", parentSessionRef: INVALID_PARENT_SESSION_REF };
|
|
61
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(value))
|
|
62
|
+
return { ok: false, redactedReason: "invalid_parent_session_binding", parentSessionRef: INVALID_PARENT_SESSION_REF };
|
|
63
|
+
return { ok: true, parentSessionRef: `ses-${value}` };
|
|
64
|
+
}
|
|
65
|
+
/** Bounded nudge text — versioned constant, never echoes user input */
|
|
66
|
+
const AGENT_TASK_NUDGE_TEXT = "Please provide your final answer now. If you have completed your analysis, output your complete response.";
|
|
67
|
+
/**
|
|
68
|
+
* Polls `session.messages` with a per-call 3-second cap so it works whether the SDK
|
|
69
|
+
* uses snapshot (returns immediately) or long-poll (blocks until output) semantics.
|
|
70
|
+
*
|
|
71
|
+
* Heartbeat: fires every `quietPeriodMs` of silence — only when inactive.
|
|
72
|
+
* Nudge: after `quietPeriodMs` of silence, sends a bounded prompt to the child
|
|
73
|
+
* session asking for the final answer. Max `maxNudges` nudges total.
|
|
74
|
+
* After exhausting nudges with no response, returns undefined.
|
|
75
|
+
*/
|
|
76
|
+
async function extractAssistantTextFromResponse(client, childSessionId, opts) {
|
|
37
77
|
const messages = client.session.messages;
|
|
38
78
|
if (messages === undefined)
|
|
39
79
|
return undefined;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
const quietPeriodMs = opts?.quietPeriodMs ?? 30_000;
|
|
81
|
+
const maxNudges = opts?.maxNudges ?? 2;
|
|
82
|
+
const MESSAGES_TIMEOUT_MS = opts?.messagesTimeoutMs ?? 3_000; // per-call cap — handles both snapshot and long-poll
|
|
83
|
+
const method = messages;
|
|
84
|
+
/**
|
|
85
|
+
* Call session.messages with a ceiling timeout so we can check inactivity periodically.
|
|
86
|
+
* This handles both snapshot APIs (return immediately) and long-poll APIs
|
|
87
|
+
* (block until LLM produces output). With the timeout, a long-poll call that
|
|
88
|
+
* hasn't returned after MESSAGES_TIMEOUT_MS resolves as null so we can
|
|
89
|
+
* check the inactivity clock and possibly send a nudge.
|
|
90
|
+
*/
|
|
91
|
+
const callMessages = () => {
|
|
92
|
+
const messagePromise = (async () => {
|
|
43
93
|
const current = await method.call(client.session, { sessionID: childSessionId });
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
94
|
+
if (isSdkErrorResponse(current))
|
|
95
|
+
return method.call(client.session, { path: { id: childSessionId } });
|
|
96
|
+
return current;
|
|
97
|
+
})();
|
|
98
|
+
// Only race against timeout when the API might block (MESSAGES_TIMEOUT_MS > 0)
|
|
99
|
+
if (MESSAGES_TIMEOUT_MS <= 0)
|
|
100
|
+
return messagePromise;
|
|
101
|
+
return Promise.race([
|
|
102
|
+
messagePromise,
|
|
103
|
+
new Promise(resolve => setTimeout(() => resolve(null), MESSAGES_TIMEOUT_MS)),
|
|
104
|
+
]);
|
|
105
|
+
};
|
|
106
|
+
/** Send a nudge to the child session with a hard timeout to prevent blocking.
|
|
107
|
+
* Uses noReply: true so the child does not generate a spurious second assistant turn.
|
|
108
|
+
*/
|
|
109
|
+
const sendNudge = async () => {
|
|
110
|
+
const promptFn = client.session.prompt ?? client.session.promptAsync;
|
|
111
|
+
if (promptFn === undefined)
|
|
112
|
+
return "skipped";
|
|
113
|
+
const NUDGE_TIMEOUT_MS = 5_000;
|
|
114
|
+
try {
|
|
115
|
+
await Promise.race([
|
|
116
|
+
promptFn.call(client.session, {
|
|
117
|
+
sessionID: childSessionId,
|
|
118
|
+
noReply: true,
|
|
119
|
+
...(opts?.runtimeModel !== undefined ? { model: opts.runtimeModel } : {}),
|
|
120
|
+
...(opts?.agentName !== undefined ? { agent: opts.agentName } : {}),
|
|
121
|
+
parts: [{ type: "text", text: AGENT_TASK_NUDGE_TEXT }],
|
|
122
|
+
}),
|
|
123
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("nudge timeout")), NUDGE_TIMEOUT_MS)),
|
|
124
|
+
]);
|
|
125
|
+
return "sent";
|
|
79
126
|
}
|
|
80
127
|
catch {
|
|
81
|
-
return
|
|
128
|
+
return "timeout";
|
|
82
129
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
};
|
|
131
|
+
const observe = (response) => {
|
|
132
|
+
if (response === null)
|
|
133
|
+
return undefined; // timed-out poll cycle
|
|
134
|
+
return observeFlowDeskAgentTaskOutputV1(response);
|
|
135
|
+
};
|
|
136
|
+
const startMs = Date.now();
|
|
137
|
+
let lastActivityMs = startMs;
|
|
138
|
+
let lastSignature = "";
|
|
139
|
+
let lastHeartbeatMs = startMs;
|
|
140
|
+
let nudgeCount = 0;
|
|
141
|
+
let latestCandidate;
|
|
142
|
+
try {
|
|
143
|
+
while (true) {
|
|
144
|
+
const response = await callMessages();
|
|
145
|
+
const nowMs = Date.now();
|
|
146
|
+
// Build signature (null response = timeout, no change)
|
|
147
|
+
const sig = response === null ? lastSignature : (() => {
|
|
148
|
+
const data = asResponseData(response);
|
|
149
|
+
const record = asRecord(data);
|
|
150
|
+
const items = Array.isArray(data) ? data
|
|
151
|
+
: Array.isArray(record?.items) ? record.items
|
|
152
|
+
: Array.isArray(record?.messages) ? record.messages : [];
|
|
153
|
+
const observed = observe(response);
|
|
154
|
+
return `${items.length}:${observed?.latestText?.length ?? 0}:${observed?.terminalObserved === true ? "terminal" : "open"}`;
|
|
155
|
+
})();
|
|
156
|
+
if (sig !== lastSignature) {
|
|
157
|
+
// New activity — reset all inactivity clocks
|
|
158
|
+
lastSignature = sig;
|
|
159
|
+
lastActivityMs = nowMs;
|
|
160
|
+
lastHeartbeatMs = nowMs;
|
|
161
|
+
}
|
|
162
|
+
const observed = observe(response);
|
|
163
|
+
if (observed?.latestText !== undefined && observed.latestText.trim().length > 0)
|
|
164
|
+
latestCandidate = observed;
|
|
165
|
+
if (observed?.terminalObserved === true && observed.latestText !== undefined && observed.latestText.trim().length > 0) {
|
|
166
|
+
return { text: observed.latestText, completionStatus: "final", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis };
|
|
167
|
+
}
|
|
168
|
+
const silenceMs = nowMs - lastActivityMs;
|
|
169
|
+
if (silenceMs >= quietPeriodMs) {
|
|
170
|
+
// Emit heartbeat on first quiet-period expiry of each silence window
|
|
171
|
+
if (nowMs - lastHeartbeatMs >= quietPeriodMs) {
|
|
172
|
+
lastHeartbeatMs = nowMs;
|
|
173
|
+
opts?.heartbeatFn?.(nowMs - startMs);
|
|
174
|
+
}
|
|
175
|
+
// Send nudge after quiet period
|
|
176
|
+
if (nudgeCount < maxNudges) {
|
|
177
|
+
nudgeCount++;
|
|
178
|
+
await sendNudge();
|
|
179
|
+
// Reset activity clock after nudge — give a fresh quiet window
|
|
180
|
+
lastActivityMs = Date.now();
|
|
181
|
+
lastHeartbeatMs = lastActivityMs;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Exhausted all nudges. Preserve usable candidate text as partial output.
|
|
185
|
+
if (latestCandidate?.latestText !== undefined && latestCandidate.latestText.trim().length > 0) {
|
|
186
|
+
return { text: latestCandidate.latestText, completionStatus: "partial", outputKind: latestCandidate.outputKind, usableForSynthesis: latestCandidate.usableForSynthesis };
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// No activity and not yet at quiet period — yield to event loop before next poll.
|
|
193
|
+
// Sleep for up to 1s or quietPeriodMs/10, whichever is smaller, to avoid tight loops
|
|
194
|
+
// while still being responsive when messages arrive quickly (snapshot mode).
|
|
195
|
+
const yieldMs = Math.max(10, Math.min(1_000, Math.floor(quietPeriodMs / 10)));
|
|
196
|
+
await new Promise(resolve => setTimeout(resolve, yieldMs));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
95
203
|
}
|
|
96
204
|
function asRecord(value) {
|
|
97
205
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -117,8 +225,38 @@ function writeSessionEvidence(input) {
|
|
|
117
225
|
record: input.record,
|
|
118
226
|
});
|
|
119
227
|
if (prepared.ok && prepared.writeIntent !== undefined) {
|
|
120
|
-
applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, [prepared.writeIntent]);
|
|
228
|
+
const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, [prepared.writeIntent]);
|
|
229
|
+
return applied.ok && applied.writtenPaths.length > 0;
|
|
121
230
|
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
function progressLabel(value) {
|
|
234
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
235
|
+
return compact.length > 120 ? `${compact.slice(0, 119)}…` : compact;
|
|
236
|
+
}
|
|
237
|
+
function writeAgentTaskProgress(input) {
|
|
238
|
+
const observedAt = input.observedAt ?? new Date().toISOString();
|
|
239
|
+
const record = {
|
|
240
|
+
schema_version: "flowdesk.agent_task_progress.v1",
|
|
241
|
+
workflow_id: input.workflowId,
|
|
242
|
+
lane_id: input.laneId,
|
|
243
|
+
task_id: input.taskId,
|
|
244
|
+
agent_ref: input.agentRef,
|
|
245
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
246
|
+
progress_seq: input.progressSeq,
|
|
247
|
+
observed_at: observedAt,
|
|
248
|
+
phase: input.phase,
|
|
249
|
+
progress_label: progressLabel(input.progressLabel),
|
|
250
|
+
progress_ref: `progress-${input.laneId}-${input.progressSeq}`,
|
|
251
|
+
redaction_version: "v1",
|
|
252
|
+
dispatch_authority_enabled: false,
|
|
253
|
+
};
|
|
254
|
+
writeSessionEvidence({
|
|
255
|
+
rootDir: input.rootDir,
|
|
256
|
+
workflowId: input.workflowId,
|
|
257
|
+
evidenceId: `agent-task-progress-${input.laneId}-${input.progressSeq}`,
|
|
258
|
+
record: record,
|
|
259
|
+
});
|
|
122
260
|
}
|
|
123
261
|
function writeAgentTaskTerminalLifecycle(input) {
|
|
124
262
|
const childSessionRef = input.childSessionRef === input.parentSessionRef ? undefined : input.childSessionRef;
|
|
@@ -154,6 +292,44 @@ function writeAgentTaskTerminalLifecycle(input) {
|
|
|
154
292
|
export async function executeFlowDeskAgentTaskV1(input) {
|
|
155
293
|
const observedAt = new Date().toISOString();
|
|
156
294
|
const token = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
295
|
+
const parentBinding = validateAgentTaskParentSessionId(input.parentSessionId);
|
|
296
|
+
const parentSessionRef = parentBinding.parentSessionRef;
|
|
297
|
+
const attemptId = `attempt-task-${token}`;
|
|
298
|
+
if (!parentBinding.ok) {
|
|
299
|
+
const taskFailedEvidenceId = `task-failed-${input.taskId}-${token}-invalid-parent`;
|
|
300
|
+
const redactedReason = parentBinding.redactedReason;
|
|
301
|
+
writeSessionEvidence({
|
|
302
|
+
rootDir: input.rootDir,
|
|
303
|
+
workflowId: input.workflowId,
|
|
304
|
+
evidenceId: taskFailedEvidenceId,
|
|
305
|
+
record: {
|
|
306
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
307
|
+
workflow_id: input.workflowId,
|
|
308
|
+
lane_id: input.laneId,
|
|
309
|
+
task_id: input.taskId,
|
|
310
|
+
agent_ref: input.agentRef,
|
|
311
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
312
|
+
failure_category: "sdk_create_failed",
|
|
313
|
+
redacted_reason: redactedReason,
|
|
314
|
+
created_at: observedAt,
|
|
315
|
+
dispatch_authority_enabled: false,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
writeAgentTaskTerminalLifecycle({
|
|
319
|
+
rootDir: input.rootDir,
|
|
320
|
+
workflowId: input.workflowId,
|
|
321
|
+
laneId: input.laneId,
|
|
322
|
+
attemptId,
|
|
323
|
+
parentSessionRef,
|
|
324
|
+
agentRef: input.agentRef,
|
|
325
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
326
|
+
state: "invocation_failed",
|
|
327
|
+
evidenceId: `lifecycle-task-terminal-${input.laneId}-${token}-invalid-parent`,
|
|
328
|
+
createdAt: observedAt,
|
|
329
|
+
updatedAt: observedAt,
|
|
330
|
+
});
|
|
331
|
+
return { status: "task_failed", failureCategory: "sdk_create_failed", redactedReason, laneId: input.laneId };
|
|
332
|
+
}
|
|
157
333
|
const launchPlan = agentTaskLaunchPlan({
|
|
158
334
|
workflowId: input.workflowId,
|
|
159
335
|
laneId: input.laneId,
|
|
@@ -163,8 +339,6 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
163
339
|
token,
|
|
164
340
|
});
|
|
165
341
|
const runningLifecycleEvidenceId = `lifecycle-task-running-${input.laneId}-${token}`;
|
|
166
|
-
const attemptId = launchPlan.attempt_id ?? `attempt-task-${token}`;
|
|
167
|
-
const parentSessionRef = `ses-${input.parentSessionId}`;
|
|
168
342
|
const promptTextTruncated = input.promptText.length > AGENT_TASK_CONTEXT_MAX_PROMPT_TEXT;
|
|
169
343
|
const agentTaskContextRecord = {
|
|
170
344
|
schema_version: "flowdesk.agent_task_context.v1",
|
|
@@ -189,17 +363,73 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
189
363
|
evidenceId: `agent-task-context-${input.taskId}-${token}`,
|
|
190
364
|
record: agentTaskContextRecord,
|
|
191
365
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
366
|
+
writeAgentTaskProgress({
|
|
367
|
+
rootDir: input.rootDir,
|
|
368
|
+
workflowId: input.workflowId,
|
|
369
|
+
laneId: input.laneId,
|
|
370
|
+
taskId: input.taskId,
|
|
371
|
+
agentRef: input.agentRef,
|
|
372
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
373
|
+
phase: "started",
|
|
374
|
+
progressSeq: 1,
|
|
375
|
+
progressLabel: "agent task lane launch started",
|
|
376
|
+
observedAt,
|
|
202
377
|
});
|
|
378
|
+
// Launch the lane — wrap in absolute timeout so session.prompt blocking doesn't hang forever.
|
|
379
|
+
// The launch phase timeout is longer (5 min) since promptAsync may queue work before responding.
|
|
380
|
+
// 1 min default — if session.prompt blocks for more than 1 min with no activity, give up
|
|
381
|
+
const LAUNCH_TIMEOUT_MS = input._launchTimeoutMs ?? 60_000;
|
|
382
|
+
const launchTimeoutHandle = setTimeout(() => { }, LAUNCH_TIMEOUT_MS);
|
|
383
|
+
const dispatchMethod = input.client.session.promptAsync !== undefined ? "promptAsync" : "prompt";
|
|
384
|
+
const launchResult = await Promise.race([
|
|
385
|
+
launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1({
|
|
386
|
+
client: input.client,
|
|
387
|
+
launchPlan,
|
|
388
|
+
request: {
|
|
389
|
+
allowActualLaneLaunch: true,
|
|
390
|
+
parentSessionId: input.parentSessionId,
|
|
391
|
+
promptText: input.promptText,
|
|
392
|
+
dispatchMethod,
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
new Promise(resolve => setTimeout(() => resolve({ status: "launch_timeout" }), LAUNCH_TIMEOUT_MS)),
|
|
396
|
+
]);
|
|
397
|
+
clearTimeout(launchTimeoutHandle);
|
|
398
|
+
if ("status" in launchResult && launchResult.status === "launch_timeout") {
|
|
399
|
+
// session.prompt blocked for too long — treat as invocation failure
|
|
400
|
+
const failedEvidenceId = `task-failed-${input.taskId}-${token}-launch-timeout`;
|
|
401
|
+
writeSessionEvidence({
|
|
402
|
+
rootDir: input.rootDir,
|
|
403
|
+
workflowId: input.workflowId,
|
|
404
|
+
evidenceId: failedEvidenceId,
|
|
405
|
+
record: {
|
|
406
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
407
|
+
workflow_id: input.workflowId,
|
|
408
|
+
lane_id: input.laneId,
|
|
409
|
+
task_id: input.taskId,
|
|
410
|
+
agent_ref: input.agentRef,
|
|
411
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
412
|
+
failure_category: "sdk_create_failed",
|
|
413
|
+
redacted_reason: "lane launch timed out: session.prompt did not respond",
|
|
414
|
+
created_at: observedAt,
|
|
415
|
+
dispatch_authority_enabled: false,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
writeAgentTaskTerminalLifecycle({
|
|
419
|
+
rootDir: input.rootDir,
|
|
420
|
+
workflowId: input.workflowId,
|
|
421
|
+
laneId: input.laneId,
|
|
422
|
+
attemptId,
|
|
423
|
+
parentSessionRef,
|
|
424
|
+
agentRef: input.agentRef,
|
|
425
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
426
|
+
state: "invocation_failed",
|
|
427
|
+
evidenceId: `lifecycle-task-terminal-${input.laneId}-${token}-launch-timeout`,
|
|
428
|
+
createdAt: observedAt,
|
|
429
|
+
updatedAt: new Date().toISOString(),
|
|
430
|
+
});
|
|
431
|
+
return { status: "task_failed", failureCategory: "sdk_create_failed", redactedReason: "launch timeout: session.prompt did not respond within the allowed window", laneId: input.laneId };
|
|
432
|
+
}
|
|
203
433
|
// Write running lifecycle evidence
|
|
204
434
|
materializeFlowDeskRuntimeLaneLaunchLifecycleEvidenceV1({
|
|
205
435
|
rootDir: input.rootDir,
|
|
@@ -278,22 +508,81 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
278
508
|
observedAt,
|
|
279
509
|
progressSummaryLabel: `agent task lane launch heartbeat`,
|
|
280
510
|
});
|
|
281
|
-
// Extract child session ID
|
|
511
|
+
// Extract child session ID
|
|
282
512
|
const childSessionId = launchResult.childSessionRef?.startsWith("ses-")
|
|
283
513
|
? launchResult.childSessionRef.slice("ses-".length)
|
|
284
514
|
: undefined;
|
|
285
|
-
|
|
515
|
+
// ── Async mode: return immediately, watchdog handles polling/nudging/abort ──
|
|
516
|
+
if (input.asyncMode === true) {
|
|
517
|
+
const resolvedChildId = childSessionId ?? "";
|
|
518
|
+
// Write child session evidence so watchdog can find it
|
|
519
|
+
writeSessionEvidence({
|
|
520
|
+
rootDir: input.rootDir,
|
|
521
|
+
workflowId: input.workflowId,
|
|
522
|
+
evidenceId: `agent-task-child-session-${input.laneId}-${token}`,
|
|
523
|
+
record: {
|
|
524
|
+
schema_version: AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION,
|
|
525
|
+
workflow_id: input.workflowId,
|
|
526
|
+
lane_id: input.laneId,
|
|
527
|
+
task_id: input.taskId,
|
|
528
|
+
child_session_id: resolvedChildId,
|
|
529
|
+
parent_session_ref: parentSessionRef,
|
|
530
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
531
|
+
agent_ref: input.agentRef,
|
|
532
|
+
nudge_count: 0,
|
|
533
|
+
last_nudge_at: null,
|
|
534
|
+
created_at: observedAt,
|
|
535
|
+
dispatch_authority_enabled: false,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
writeAgentTaskProgress({
|
|
539
|
+
rootDir: input.rootDir,
|
|
540
|
+
workflowId: input.workflowId,
|
|
541
|
+
laneId: input.laneId,
|
|
542
|
+
taskId: input.taskId,
|
|
543
|
+
agentRef: input.agentRef,
|
|
544
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
545
|
+
phase: "waiting",
|
|
546
|
+
progressSeq: 2,
|
|
547
|
+
progressLabel: "agent task waiting for async child result",
|
|
548
|
+
});
|
|
549
|
+
return { status: "task_launched", laneId: input.laneId, childSessionId: resolvedChildId };
|
|
550
|
+
}
|
|
551
|
+
let resultObservation;
|
|
286
552
|
if (childSessionId !== undefined) {
|
|
287
|
-
|
|
553
|
+
const runtimeModel = launchResult.status === "lane_launch_started" && typeof launchResult.model === "string"
|
|
554
|
+
? launchResult.model : undefined;
|
|
555
|
+
const agentName = launchResult.status === "lane_launch_started" && typeof launchResult.agent === "string"
|
|
556
|
+
? launchResult.agent : undefined;
|
|
557
|
+
resultObservation = await extractAssistantTextFromResponse(input.client, childSessionId, {
|
|
558
|
+
quietPeriodMs: input._nudgeQuietPeriodMs ?? 20_000, // default 20s per policy
|
|
559
|
+
maxNudges: 2,
|
|
560
|
+
runtimeModel,
|
|
561
|
+
agentName,
|
|
562
|
+
messagesTimeoutMs: input._messagesTimeoutMs,
|
|
563
|
+
heartbeatFn: (elapsedMs) => {
|
|
564
|
+
recordFlowDeskLaneHeartbeatV1({
|
|
565
|
+
rootDir: input.rootDir,
|
|
566
|
+
workflowId: input.workflowId,
|
|
567
|
+
attemptId,
|
|
568
|
+
laneId: input.laneId,
|
|
569
|
+
parentSessionRef,
|
|
570
|
+
agentRef: input.agentRef,
|
|
571
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
572
|
+
state: "running",
|
|
573
|
+
observedAt: new Date().toISOString(),
|
|
574
|
+
progressSummaryLabel: `agent task waiting for response elapsed=${Math.floor(elapsedMs / 1000)}s`,
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
});
|
|
288
578
|
}
|
|
289
|
-
|
|
579
|
+
const resultText = resultObservation?.text;
|
|
580
|
+
if (resultText === undefined) {
|
|
290
581
|
// No response text - write task_failed
|
|
291
582
|
const taskFailedEvidenceId = `task-failed-${input.taskId}-${token}`;
|
|
292
|
-
const failureCategory =
|
|
293
|
-
const evidenceFailureCategory =
|
|
294
|
-
const redactedReason =
|
|
295
|
-
? "lane launched but no assistant response text found"
|
|
296
|
-
: "lane launched but final assistant response did not satisfy requested output contract";
|
|
583
|
+
const failureCategory = "no_response";
|
|
584
|
+
const evidenceFailureCategory = "no_response";
|
|
585
|
+
const redactedReason = "lane launched but no assistant response text found";
|
|
297
586
|
const taskFailedRecord = {
|
|
298
587
|
schema_version: "flowdesk.task_failed.v1",
|
|
299
588
|
workflow_id: input.workflowId,
|
|
@@ -312,6 +601,17 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
312
601
|
evidenceId: taskFailedEvidenceId,
|
|
313
602
|
record: taskFailedRecord,
|
|
314
603
|
});
|
|
604
|
+
writeAgentTaskProgress({
|
|
605
|
+
rootDir: input.rootDir,
|
|
606
|
+
workflowId: input.workflowId,
|
|
607
|
+
laneId: input.laneId,
|
|
608
|
+
taskId: input.taskId,
|
|
609
|
+
agentRef: input.agentRef,
|
|
610
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
611
|
+
phase: "failed",
|
|
612
|
+
progressSeq: 3,
|
|
613
|
+
progressLabel: failureCategory === "no_response" ? "agent task finished without response" : "agent task output contract not satisfied",
|
|
614
|
+
});
|
|
315
615
|
writeAgentTaskTerminalLifecycle({
|
|
316
616
|
rootDir: input.rootDir,
|
|
317
617
|
workflowId: input.workflowId,
|
|
@@ -322,7 +622,7 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
322
622
|
messageRef: launchResult.messageRef?.startsWith("msg-") ? launchResult.messageRef : undefined,
|
|
323
623
|
agentRef: input.agentRef,
|
|
324
624
|
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
325
|
-
state:
|
|
625
|
+
state: "no_output",
|
|
326
626
|
evidenceId: `lifecycle-task-terminal-${input.laneId}-${token}`,
|
|
327
627
|
createdAt: observedAt,
|
|
328
628
|
updatedAt: new Date().toISOString(),
|
|
@@ -335,10 +635,9 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
335
635
|
laneId: input.laneId,
|
|
336
636
|
};
|
|
337
637
|
}
|
|
338
|
-
// Truncate if needed
|
|
339
638
|
const fullResultText = resultText;
|
|
340
|
-
const
|
|
341
|
-
const storedResultText =
|
|
639
|
+
const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(fullResultText);
|
|
640
|
+
const storedResultText = sanitizedResult.text;
|
|
342
641
|
const promptSha256 = sha256Hex(input.promptText);
|
|
343
642
|
const resultSha256 = sha256Hex(fullResultText);
|
|
344
643
|
// Write task_result evidence
|
|
@@ -352,17 +651,42 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
352
651
|
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
353
652
|
task_prompt_sha256: promptSha256,
|
|
354
653
|
result_text: storedResultText,
|
|
355
|
-
result_text_truncated: truncated,
|
|
654
|
+
result_text_truncated: sanitizedResult.truncated,
|
|
356
655
|
result_text_sha256: resultSha256,
|
|
656
|
+
completion_status: resultObservation?.completionStatus ?? "final",
|
|
657
|
+
output_kind: resultObservation?.outputKind ?? "final_answer",
|
|
658
|
+
usable_for_synthesis: resultObservation?.usableForSynthesis ?? true,
|
|
659
|
+
missing_contract: input.outputContract === "final_assistant_text" &&
|
|
660
|
+
(resultObservation?.completionStatus !== "final" ||
|
|
661
|
+
["empty", "process_notes", "tool_trace_only"].includes(String(resultObservation?.outputKind ?? ""))),
|
|
357
662
|
created_at: observedAt,
|
|
358
663
|
dispatch_authority_enabled: false,
|
|
359
664
|
};
|
|
360
|
-
writeSessionEvidence({
|
|
665
|
+
const taskResultWritten = writeSessionEvidence({
|
|
361
666
|
rootDir: input.rootDir,
|
|
362
667
|
workflowId: input.workflowId,
|
|
363
668
|
evidenceId: taskResultEvidenceId,
|
|
364
669
|
record: taskResultRecord,
|
|
365
670
|
});
|
|
671
|
+
if (!taskResultWritten) {
|
|
672
|
+
return {
|
|
673
|
+
status: "task_failed",
|
|
674
|
+
failureCategory: "unknown",
|
|
675
|
+
redactedReason: "task_result evidence persistence failed",
|
|
676
|
+
laneId: input.laneId,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
writeAgentTaskProgress({
|
|
680
|
+
rootDir: input.rootDir,
|
|
681
|
+
workflowId: input.workflowId,
|
|
682
|
+
laneId: input.laneId,
|
|
683
|
+
taskId: input.taskId,
|
|
684
|
+
agentRef: input.agentRef,
|
|
685
|
+
providerQualifiedModelId: input.providerQualifiedModelId,
|
|
686
|
+
phase: "finalizing",
|
|
687
|
+
progressSeq: 3,
|
|
688
|
+
progressLabel: "agent task result captured",
|
|
689
|
+
});
|
|
366
690
|
writeAgentTaskTerminalLifecycle({
|
|
367
691
|
rootDir: input.rootDir,
|
|
368
692
|
workflowId: input.workflowId,
|
|
@@ -380,6 +704,11 @@ export async function executeFlowDeskAgentTaskV1(input) {
|
|
|
380
704
|
updatedAt: new Date().toISOString(),
|
|
381
705
|
timeoutMs: input.timeoutMs,
|
|
382
706
|
});
|
|
707
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
708
|
+
rootDir: input.rootDir,
|
|
709
|
+
workflowId: input.workflowId,
|
|
710
|
+
observedAt,
|
|
711
|
+
});
|
|
383
712
|
return {
|
|
384
713
|
status: "task_completed",
|
|
385
714
|
resultText: fullResultText,
|