@flowdesk/opencode-plugin 0.1.13 → 0.1.15
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 +29 -0
- package/dist/agent-task-output.d.ts.map +1 -0
- package/dist/agent-task-output.js +225 -0
- package/dist/agent-task-output.js.map +1 -0
- package/dist/agent-task-runner.d.ts +34 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +634 -84
- 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 +390 -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 +257 -0
- package/dist/event-hook-observer.js.map +1 -0
- package/dist/managed-dispatch-adapter.d.ts +62 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +472 -4
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/model-selection-engine.d.ts +60 -0
- package/dist/model-selection-engine.d.ts.map +1 -0
- package/dist/model-selection-engine.js +242 -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 +262 -33
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +497 -20
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +34 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +680 -3
- 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 +449 -44
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-subtask-activity.d.ts +73 -0
- package/dist/tui-subtask-activity.d.ts.map +1 -0
- package/dist/tui-subtask-activity.js +271 -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 +275 -8
- package/dist/tui-usage-snapshot.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +102 -44
- 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 +135 -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 +12 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -1
- package/dist/workflow-dispatch-tool.js +31 -3
- 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
package/dist/server.js
CHANGED
|
@@ -4,18 +4,23 @@ import { join, resolve, sep } from "node:path";
|
|
|
4
4
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
|
|
5
5
|
import { tool, } from "@opencode-ai/plugin";
|
|
6
6
|
import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration, } from "./index.js";
|
|
7
|
+
import { observeFlowDeskOpenCodeEventV1 } from "./event-hook-observer.js";
|
|
7
8
|
import { recordFlowDeskLaneHeartbeatV1, } from "./lane-heartbeat-writer.js";
|
|
8
9
|
import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile, hasFlowDeskLocalPlanningEvidenceV1, } from "./local-adapter.js";
|
|
9
|
-
import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
|
|
10
|
+
import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, observeAndFinalizeManagedDispatchLaneV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
|
|
10
11
|
import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.js";
|
|
12
|
+
import { formatFlowDeskTuiUsageSnapshotCompactText, loadFlowDeskTuiUsageSnapshotViewV1, } from "./tui-usage-snapshot.js";
|
|
11
13
|
import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
|
|
12
14
|
import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
|
|
13
15
|
import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
|
|
14
16
|
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
17
|
+
import { executeFlowDeskAutoContinuePreviewToolV1, } from "./auto-continue-preview-tool.js";
|
|
15
18
|
import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
|
|
16
19
|
import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
|
|
17
20
|
import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
|
|
18
21
|
import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
|
|
22
|
+
import { executeFlowDeskWorkflowOrchestratorV1 } from "./workflow-orchestrator.js";
|
|
23
|
+
import { executeFlowDeskWorkflowSynthesisPreviewV1 } from "./workflow-synthesis-tool.js";
|
|
19
24
|
import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
|
|
20
25
|
import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
|
|
21
26
|
import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
|
|
@@ -40,10 +45,12 @@ export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
|
|
|
40
45
|
export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
|
|
41
46
|
export const flowdeskWorkflowDispatchOption = "workflowDispatch";
|
|
42
47
|
export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
|
|
48
|
+
export const flowdeskUiProbeOption = "uiProbe";
|
|
43
49
|
export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
|
|
44
50
|
export const flowdeskWatchdogOption = "watchdog";
|
|
45
51
|
export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
|
|
46
52
|
export const flowdeskManagedDispatchBetaToolName = "flowdesk_managed_dispatch_beta";
|
|
53
|
+
export const flowdeskManagedDispatchLaneFinalizeToolName = "flowdesk_managed_dispatch_lane_finalize";
|
|
47
54
|
export const flowdeskExactModelProviderAcquisitionLiveTestToolName = "flowdesk_exact_model_provider_acquisition_live_test";
|
|
48
55
|
export const flowdeskRuntimeReviewerExecutionToolName = "flowdesk_runtime_reviewer_execution";
|
|
49
56
|
export const flowdeskManagedFallbackRegateToolName = "flowdesk_managed_fallback_regate";
|
|
@@ -57,6 +64,9 @@ export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
|
|
|
57
64
|
export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
|
|
58
65
|
export const flowdeskAgentTaskRunOption = "agentTaskRun";
|
|
59
66
|
export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
|
|
67
|
+
export const flowdeskWorkflowSynthesisPreviewToolName = "flowdesk_workflow_synthesis_preview";
|
|
68
|
+
export const flowdeskAutoContinuePreviewToolName = "flowdesk_auto_continue_preview";
|
|
69
|
+
export const flowdeskUiProbeToolName = "flowdesk_ui_probe";
|
|
60
70
|
const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
|
|
61
71
|
const disabledAuthority = {
|
|
62
72
|
productionRegistrationEligible: false,
|
|
@@ -537,6 +547,15 @@ function redactedManagedDispatchBetaToolResult(result) {
|
|
|
537
547
|
sessionId: result.sessionId,
|
|
538
548
|
agent: result.agent,
|
|
539
549
|
model: result.model,
|
|
550
|
+
...(!("laneId" in result) || result.laneId === undefined
|
|
551
|
+
? {}
|
|
552
|
+
: { laneId: result.laneId }),
|
|
553
|
+
...(!("childSessionRef" in result) || result.childSessionRef === undefined
|
|
554
|
+
? {}
|
|
555
|
+
: { childSessionRef: result.childSessionRef }),
|
|
556
|
+
...(!("messageRef" in result) || result.messageRef === undefined
|
|
557
|
+
? {}
|
|
558
|
+
: { messageRef: result.messageRef }),
|
|
540
559
|
...("redactedErrorCategory" in result
|
|
541
560
|
? { redactedErrorCategory: result.redactedErrorCategory }
|
|
542
561
|
: {}),
|
|
@@ -886,6 +905,9 @@ async function evaluateFlowDeskManagedDispatchRunRoute(request, options = {}) {
|
|
|
886
905
|
...(options.reservationStore === undefined
|
|
887
906
|
? {}
|
|
888
907
|
: { reservationStore: options.reservationStore }),
|
|
908
|
+
...(options.durableStateRootDir === undefined
|
|
909
|
+
? {}
|
|
910
|
+
: { durableStateRootDir: options.durableStateRootDir }),
|
|
889
911
|
});
|
|
890
912
|
return {
|
|
891
913
|
runRouteProfile: "flowdesk_run_default_managed_dispatch_route",
|
|
@@ -944,10 +966,46 @@ export function createFlowDeskManagedDispatchBetaOptInTools(client, reservationS
|
|
|
944
966
|
dispatchManifest: record.dispatchManifest,
|
|
945
967
|
...(reloadedEvidence === undefined ? {} : { reloadedEvidence }),
|
|
946
968
|
...(reservationStore === undefined ? {} : { reservationStore }),
|
|
969
|
+
...(durableStateRootDir === undefined ? {} : { durableStateRootDir }),
|
|
947
970
|
});
|
|
948
971
|
return JSON.stringify(redactedManagedDispatchBetaToolResult(result));
|
|
949
972
|
},
|
|
950
973
|
}),
|
|
974
|
+
[flowdeskManagedDispatchLaneFinalizeToolName]: tool({
|
|
975
|
+
description: "FlowDesk managed-dispatch lane finalize observer; reads a launched managed-dispatch lane's child session once and records terminal task_result + lane_lifecycle evidence. Observation-only: never nudges, aborts, or enables dispatch/provider/runtime/fallback authority.",
|
|
976
|
+
args: {
|
|
977
|
+
workflowId: tool.schema.string().describe("Workflow id that owns the managed-dispatch lane."),
|
|
978
|
+
laneId: tool.schema.string().describe("Lane id of the launched managed-dispatch lane."),
|
|
979
|
+
attemptId: tool.schema.string().describe("Attempt id bound to the lane."),
|
|
980
|
+
childSessionId: tool.schema.string().describe("Child session id to observe (raw OpenCode id)."),
|
|
981
|
+
agentRef: tool.schema.string().describe("Agent ref bound to the lane."),
|
|
982
|
+
providerQualifiedModelId: tool.schema.string().describe("Concrete provider-qualified model id for the lane."),
|
|
983
|
+
parentSessionRef: tool.schema.string().optional().describe("Optional parent ses- ref for lifecycle evidence."),
|
|
984
|
+
},
|
|
985
|
+
async execute(input) {
|
|
986
|
+
const record = isRecord(input) ? input : {};
|
|
987
|
+
if (durableStateRootDir === undefined) {
|
|
988
|
+
return JSON.stringify({
|
|
989
|
+
adapterProfile: "managed_dispatch_lane_finalize_observer",
|
|
990
|
+
status: "blocked_before_finalize",
|
|
991
|
+
redactedBlockReason: "managed-dispatch lane finalize requires a configured durable state root.",
|
|
992
|
+
authority: { ...disabledAuthority, toolAuthority: false },
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
const result = await observeAndFinalizeManagedDispatchLaneV1({
|
|
996
|
+
client,
|
|
997
|
+
rootDir: durableStateRootDir,
|
|
998
|
+
workflowId: String(record.workflowId ?? ""),
|
|
999
|
+
laneId: String(record.laneId ?? ""),
|
|
1000
|
+
attemptId: String(record.attemptId ?? ""),
|
|
1001
|
+
childSessionId: String(record.childSessionId ?? ""),
|
|
1002
|
+
agentRef: String(record.agentRef ?? "agent-managed-dispatch"),
|
|
1003
|
+
providerQualifiedModelId: String(record.providerQualifiedModelId ?? "unknown/unknown"),
|
|
1004
|
+
...(typeof record.parentSessionRef === "string" ? { parentSessionRef: record.parentSessionRef } : {}),
|
|
1005
|
+
});
|
|
1006
|
+
return JSON.stringify(result);
|
|
1007
|
+
},
|
|
1008
|
+
}),
|
|
951
1009
|
};
|
|
952
1010
|
}
|
|
953
1011
|
export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(client, rootDir, cacheMaterialization, runtimeReviewerExecutionClient) {
|
|
@@ -1175,9 +1233,11 @@ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(cl
|
|
|
1175
1233
|
}),
|
|
1176
1234
|
};
|
|
1177
1235
|
}
|
|
1178
|
-
export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot) {
|
|
1236
|
+
export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot, providerUsageLiveConfig) {
|
|
1179
1237
|
const recentSuggestionCards = new Map();
|
|
1180
1238
|
const recentStallAlerts = new Map();
|
|
1239
|
+
const usageAutoRefreshMaxAgeMs = 3 * 60_000;
|
|
1240
|
+
let lastUsageRefreshAttemptAtMs = 0;
|
|
1181
1241
|
return async function message(input, output) {
|
|
1182
1242
|
const inputRecord = isRecord(input) ? input : {};
|
|
1183
1243
|
const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
|
|
@@ -1192,6 +1252,40 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1192
1252
|
const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
|
|
1193
1253
|
const preview = previewNaturalLanguageRouting(request, session);
|
|
1194
1254
|
const nowMs = clockMs(now);
|
|
1255
|
+
if (providerUsageLiveConfig?.durableStateRootDir && nowMs - lastUsageRefreshAttemptAtMs > 30_000) {
|
|
1256
|
+
lastUsageRefreshAttemptAtMs = nowMs;
|
|
1257
|
+
let isStale = false;
|
|
1258
|
+
try {
|
|
1259
|
+
const cachePath = join(providerUsageLiveConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
|
|
1260
|
+
const cacheContent = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
1261
|
+
if (typeof cacheContent.observed_at === "string" && nowMs - Date.parse(cacheContent.observed_at) > usageAutoRefreshMaxAgeMs)
|
|
1262
|
+
isStale = true;
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
isStale = true;
|
|
1266
|
+
}
|
|
1267
|
+
if (isStale) {
|
|
1268
|
+
try {
|
|
1269
|
+
await executeFlowDeskProviderUsageLiveV1({ config: { ...providerUsageLiveConfig, persistSidebarCache: true }, request: { providerFamily: "all" } });
|
|
1270
|
+
}
|
|
1271
|
+
catch { }
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const usageTextToAppend = providerUsageLiveConfig?.durableStateRootDir &&
|
|
1275
|
+
providerUsageLiveConfig.appendToChat === true
|
|
1276
|
+
? formatFlowDeskTuiUsageSnapshotCompactText(loadFlowDeskTuiUsageSnapshotViewV1({
|
|
1277
|
+
rootDir: providerUsageLiveConfig.durableStateRootDir,
|
|
1278
|
+
workflowId: providerUsageLiveConfig.persistWorkflowId,
|
|
1279
|
+
now: () => (typeof now === "function" ? now() : now),
|
|
1280
|
+
}))
|
|
1281
|
+
: undefined;
|
|
1282
|
+
const appendUsageCard = () => {
|
|
1283
|
+
if (usageTextToAppend === undefined)
|
|
1284
|
+
return;
|
|
1285
|
+
if (!Array.isArray(output.parts))
|
|
1286
|
+
output.parts = [];
|
|
1287
|
+
output.parts.push(buildTextPart(usageTextToAppend));
|
|
1288
|
+
};
|
|
1195
1289
|
for (const [key, recordedAtMs] of recentSuggestionCards) {
|
|
1196
1290
|
if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
|
|
1197
1291
|
nowMs < recordedAtMs)
|
|
@@ -1228,7 +1322,8 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1228
1322
|
summary.totalLate > 0;
|
|
1229
1323
|
const progressCardReady = stallAlert?.includeProgressCards === true &&
|
|
1230
1324
|
summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
|
|
1231
|
-
|
|
1325
|
+
const autoNextReady = summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true);
|
|
1326
|
+
if (stalledAlertReady || lateAlertReady || progressCardReady || autoNextReady) {
|
|
1232
1327
|
stallDedupKey = stallAlertDuplicateKey(request, summary);
|
|
1233
1328
|
stallTextToAppend = stallAlertText(summary);
|
|
1234
1329
|
}
|
|
@@ -1246,6 +1341,7 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1246
1341
|
}
|
|
1247
1342
|
};
|
|
1248
1343
|
if (preview.evaluation.response.route_decision === "continue_chat") {
|
|
1344
|
+
appendUsageCard();
|
|
1249
1345
|
appendStallCard();
|
|
1250
1346
|
return;
|
|
1251
1347
|
}
|
|
@@ -1256,13 +1352,16 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1256
1352
|
recentSuggestionCards.set(duplicateKey, nowMs);
|
|
1257
1353
|
writeDurableSuggestionPreference(durableSuggestionRoot, duplicateKey, request, preview.evaluation.response, nowMs);
|
|
1258
1354
|
if (previousAtMs !== undefined &&
|
|
1259
|
-
nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
|
|
1355
|
+
nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs) {
|
|
1356
|
+
appendUsageCard();
|
|
1260
1357
|
return;
|
|
1358
|
+
}
|
|
1261
1359
|
}
|
|
1262
1360
|
const result = evaluateNaturalLanguageRouting(request, session);
|
|
1263
1361
|
if (!Array.isArray(output.parts))
|
|
1264
1362
|
output.parts = [];
|
|
1265
1363
|
output.parts.push(buildTextPart(steeringText(result)));
|
|
1364
|
+
appendUsageCard();
|
|
1266
1365
|
appendStallCard();
|
|
1267
1366
|
};
|
|
1268
1367
|
}
|
|
@@ -1361,12 +1460,14 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1361
1460
|
const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
|
|
1362
1461
|
const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
|
|
1363
1462
|
const primary = stalledEntry ?? lateEntry;
|
|
1364
|
-
const
|
|
1463
|
+
const autoNextReady = workflow.laneProgressAggregate?.autoNextStepEligible === true;
|
|
1464
|
+
const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => (autoNextReady || lane.classification !== "terminal") &&
|
|
1365
1465
|
laneInCurrentSession(lane.laneId));
|
|
1366
1466
|
const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
|
|
1367
1467
|
const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
|
|
1368
1468
|
const shouldShowWorkflow = scopedStalledCount > 0 ||
|
|
1369
1469
|
(stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
|
|
1470
|
+
(autoNextReady && scopedLaneCards.length > 0) ||
|
|
1370
1471
|
(stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
|
|
1371
1472
|
if (!shouldShowWorkflow)
|
|
1372
1473
|
return undefined;
|
|
@@ -1443,18 +1544,36 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1443
1544
|
...(primary?.failureHint === undefined
|
|
1444
1545
|
? {}
|
|
1445
1546
|
: { failureHint: primary.failureHint }),
|
|
1547
|
+
...(workflow.laneProgressAggregate === undefined
|
|
1548
|
+
? {}
|
|
1549
|
+
: { laneProgressAggregate: workflow.laneProgressAggregate }),
|
|
1550
|
+
...(autoNextReady ? { autoNextReady: true } : {}),
|
|
1551
|
+
...(workflow.latestWorkflowSynthesisTasksSummarized === undefined
|
|
1552
|
+
? {}
|
|
1553
|
+
: { synthesisTasksSummarized: workflow.latestWorkflowSynthesisTasksSummarized }),
|
|
1554
|
+
...(workflow.latestWorkflowSynthesisConflictDetected === undefined
|
|
1555
|
+
? {}
|
|
1556
|
+
: { synthesisConflictDetected: workflow.latestWorkflowSynthesisConflictDetected }),
|
|
1446
1557
|
...(stallAlert.includeProgressCards === true
|
|
1447
1558
|
? {
|
|
1448
1559
|
laneCards: scopedLaneCards
|
|
1449
1560
|
.slice(0, stallAlert.maxProgressCards ?? 3)
|
|
1450
1561
|
.map((lane) => ({
|
|
1451
1562
|
laneId: lane.laneId,
|
|
1563
|
+
taskId: lane.taskId,
|
|
1452
1564
|
state: lane.state,
|
|
1453
1565
|
classification: lane.classification,
|
|
1454
1566
|
secondsSinceLastSignal: lane.secondsSinceLastSignal,
|
|
1455
1567
|
agentRef: lane.agentRef,
|
|
1456
1568
|
providerQualifiedModelId: lane.providerQualifiedModelId,
|
|
1569
|
+
promptPreview: lane.promptPreview,
|
|
1570
|
+
nudgeCount: lane.nudgeCount,
|
|
1571
|
+
progressPhase: lane.progressPhase,
|
|
1572
|
+
progressLabel: lane.progressLabel,
|
|
1457
1573
|
verdictLabel: lane.verdictLabel,
|
|
1574
|
+
completionStatus: lane.completionStatus,
|
|
1575
|
+
outputKind: lane.outputKind,
|
|
1576
|
+
usableForSynthesis: lane.usableForSynthesis,
|
|
1458
1577
|
failureHint: lane.failureHint,
|
|
1459
1578
|
})),
|
|
1460
1579
|
}
|
|
@@ -1524,6 +1643,9 @@ function stallAlertText(summary) {
|
|
|
1524
1643
|
else if (summary.worstClassification === "progressing_late") {
|
|
1525
1644
|
lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
|
|
1526
1645
|
}
|
|
1646
|
+
else if (summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true)) {
|
|
1647
|
+
lines.push("All FlowDesk subtasks completed normally. Auto-next synthesis is ready.");
|
|
1648
|
+
}
|
|
1527
1649
|
else if (progressCardCount > 0) {
|
|
1528
1650
|
lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
|
|
1529
1651
|
}
|
|
@@ -1534,19 +1656,56 @@ function stallAlertText(summary) {
|
|
|
1534
1656
|
const secs = workflow.secondsSinceLastSignal ?? 0;
|
|
1535
1657
|
const minutes = Math.floor(secs / 60);
|
|
1536
1658
|
const hint = workflow.failureHint ?? "no recent heartbeat";
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1659
|
+
if (workflow.autoNextReady === true) {
|
|
1660
|
+
const synthesis = workflow.synthesisTasksSummarized === undefined
|
|
1661
|
+
? "synthesis not yet recorded"
|
|
1662
|
+
: `synthesis recorded for ${workflow.synthesisTasksSummarized} task(s)`;
|
|
1663
|
+
const conflict = workflow.synthesisConflictDetected === undefined
|
|
1664
|
+
? " conflict=unknown"
|
|
1665
|
+
: workflow.synthesisConflictDetected === true ? " conflict=true" : " conflict=false";
|
|
1666
|
+
const nextAction = workflow.laneProgressAggregate?.nextActionAvailable === true
|
|
1667
|
+
? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
|
|
1668
|
+
: "";
|
|
1669
|
+
lines.push(`- workflow ${workflow.workflowId}: auto-next ready (${synthesis},${conflict}${nextAction}).`);
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
const counts = workflow.stalledLaneCount > 0
|
|
1673
|
+
? `${workflow.stalledLaneCount} stalled`
|
|
1674
|
+
: `${workflow.lateLaneCount} progressing-late`;
|
|
1675
|
+
lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
|
|
1676
|
+
}
|
|
1677
|
+
if (workflow.laneProgressAggregate !== undefined) {
|
|
1678
|
+
const aggregate = workflow.laneProgressAggregate;
|
|
1679
|
+
const nextAction = aggregate.nextActionAvailable === true
|
|
1680
|
+
? `, next_action=${aggregate.nextActionKind ?? "available"}_ready`
|
|
1681
|
+
: "";
|
|
1682
|
+
lines.push(` tasks: expected=${aggregate.expected}, terminal=${aggregate.terminal}, completed=${aggregate.normalCompleted}, failed=${aggregate.failed}, awaiting_permission=${aggregate.awaitingPermission}, auto_next=${aggregate.autoNextStepEligible}${nextAction}`);
|
|
1683
|
+
}
|
|
1541
1684
|
for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
|
|
1542
1685
|
const age = lane.secondsSinceLastSignal === undefined
|
|
1543
1686
|
? "unknown"
|
|
1544
1687
|
: `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
|
|
1545
1688
|
const model = lane.providerQualifiedModelId ?? "(unknown)";
|
|
1546
1689
|
const agent = lane.agentRef ?? "(unknown)";
|
|
1690
|
+
const task = lane.taskId ?? lane.laneId;
|
|
1691
|
+
const prompt = lane.promptPreview ?? "(hidden)";
|
|
1692
|
+
const nudge = lane.nudgeCount === undefined ? "?" : String(lane.nudgeCount);
|
|
1693
|
+
const progress = lane.progressLabel === undefined
|
|
1694
|
+
? "(none)"
|
|
1695
|
+
: `${lane.progressPhase ?? "progress"}: ${lane.progressLabel}`;
|
|
1547
1696
|
const verdict = lane.verdictLabel ?? "(none)";
|
|
1697
|
+
const resultQuality = lane.completionStatus === undefined && lane.outputKind === undefined && lane.usableForSynthesis === undefined
|
|
1698
|
+
? "(none)"
|
|
1699
|
+
: `${lane.completionStatus ?? "unknown"}/${lane.outputKind ?? "unknown"}/synthesis=${lane.usableForSynthesis === undefined ? "unknown" : String(lane.usableForSynthesis)}`;
|
|
1548
1700
|
const issue = lane.failureHint === undefined ? "" : ` issue=${lane.failureHint}`;
|
|
1549
|
-
lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}
|
|
1701
|
+
lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}`);
|
|
1702
|
+
lines.push(` task: ${task}`);
|
|
1703
|
+
lines.push(` prompt: ${prompt}`);
|
|
1704
|
+
lines.push(` agent: ${agent}`);
|
|
1705
|
+
lines.push(` model: ${model}`);
|
|
1706
|
+
lines.push(` progress: ${progress}`);
|
|
1707
|
+
lines.push(` result: ${resultQuality}`);
|
|
1708
|
+
lines.push(` last signal: ${age}; nudges=${nudge}; verdict=${verdict}${issue}`);
|
|
1550
1709
|
}
|
|
1551
1710
|
}
|
|
1552
1711
|
if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
|
|
@@ -2074,6 +2233,32 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2074
2233
|
return undefined;
|
|
2075
2234
|
const client = input.client;
|
|
2076
2235
|
const rootDir = input.durableStateRoot;
|
|
2236
|
+
const promptPreview = (text, max = 120) => {
|
|
2237
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
2238
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
|
|
2239
|
+
};
|
|
2240
|
+
const taskSummaryForUser = (input) => {
|
|
2241
|
+
const lines = [
|
|
2242
|
+
input.status === "task_completed"
|
|
2243
|
+
? "FlowDesk task completed."
|
|
2244
|
+
: input.status === "task_launched"
|
|
2245
|
+
? "FlowDesk task launched."
|
|
2246
|
+
: "FlowDesk task failed.",
|
|
2247
|
+
`workflow: ${input.workflowId}`,
|
|
2248
|
+
`lane: ${input.laneId}`,
|
|
2249
|
+
`task: ${input.taskId}`,
|
|
2250
|
+
`prompt: ${promptPreview(input.promptText)}`,
|
|
2251
|
+
`agent: ${input.agentName}`,
|
|
2252
|
+
`model: ${input.providerQualifiedModelId}`,
|
|
2253
|
+
];
|
|
2254
|
+
if (input.asyncMode === true)
|
|
2255
|
+
lines.push("progress: use /flowdesk-status or flowdesk_status_live to follow durable lane evidence");
|
|
2256
|
+
if (input.resultText !== undefined)
|
|
2257
|
+
lines.push(`result: ${promptPreview(input.resultText, 200)}`);
|
|
2258
|
+
if (input.failureCategory !== undefined)
|
|
2259
|
+
lines.push(`failure: ${input.failureCategory}${input.redactedReason === undefined ? "" : ` (${input.redactedReason})`}`);
|
|
2260
|
+
return lines.join("\n");
|
|
2261
|
+
};
|
|
2077
2262
|
return {
|
|
2078
2263
|
[flowdeskAgentTaskRunToolName]: tool({
|
|
2079
2264
|
description: [
|
|
@@ -2081,7 +2266,7 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2081
2266
|
"Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
|
|
2082
2267
|
"Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
|
|
2083
2268
|
"WHEN TO USE: user asks to delegate a specific task to a specific model/agent.",
|
|
2084
|
-
"WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch)
|
|
2269
|
+
"WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch).",
|
|
2085
2270
|
"After calling, use flowdesk_status_live to check the lane status.",
|
|
2086
2271
|
].join(" "),
|
|
2087
2272
|
args: {
|
|
@@ -2092,6 +2277,8 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2092
2277
|
parentSessionId: tool.schema.string().optional().describe("Parent session id"),
|
|
2093
2278
|
developerModeAcknowledged: tool.schema.boolean(),
|
|
2094
2279
|
allowProviderCall: tool.schema.boolean(),
|
|
2280
|
+
nudgeQuietPeriodMs: tool.schema.number().optional().describe("Milliseconds of silence before sending a nudge prompt. Default 10000ms (10s). Recommended: always pass 10000. At 10s silence → nudge 1, 20s → nudge 2, 30s+ → lane fails and watchdog retries."),
|
|
2281
|
+
asyncMode: tool.schema.boolean().optional().describe("When true, return laneId immediately after launch. Watchdog polls child session, sends noReply nudges at 10s/20s, and aborts at 30s+. Coordinator uses flowdesk_status_live to detect completion. Recommended for all orchestration calls."),
|
|
2095
2282
|
},
|
|
2096
2283
|
async execute(args, ctx) {
|
|
2097
2284
|
const record = isRecord(args) ? args : {};
|
|
@@ -2111,6 +2298,9 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2111
2298
|
: typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
|
|
2112
2299
|
? ctxRecord.sessionID
|
|
2113
2300
|
: "";
|
|
2301
|
+
const nudgeQuietPeriodMs = typeof record.nudgeQuietPeriodMs === "number" && record.nudgeQuietPeriodMs > 0
|
|
2302
|
+
? Math.floor(record.nudgeQuietPeriodMs) : undefined;
|
|
2303
|
+
const asyncMode = record.asyncMode === true;
|
|
2114
2304
|
const taskId = `task-${Date.now().toString(36)}`;
|
|
2115
2305
|
const laneId = `lane-task-${Date.now().toString(36)}`;
|
|
2116
2306
|
const result = await executeFlowDeskAgentTaskV1({
|
|
@@ -2123,19 +2313,37 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2123
2313
|
parentSessionId,
|
|
2124
2314
|
rootDir,
|
|
2125
2315
|
client,
|
|
2316
|
+
asyncMode,
|
|
2317
|
+
_nudgeQuietPeriodMs: nudgeQuietPeriodMs,
|
|
2126
2318
|
});
|
|
2319
|
+
const failureCategory = result.status === "task_failed" ? result.failureCategory : undefined;
|
|
2320
|
+
const redactedReason = result.status === "task_failed" ? result.redactedReason : undefined;
|
|
2127
2321
|
return JSON.stringify({
|
|
2128
2322
|
workflowId,
|
|
2129
2323
|
laneId,
|
|
2130
2324
|
taskId,
|
|
2131
2325
|
status: result.status,
|
|
2326
|
+
taskPreview: promptPreview(taskDescription),
|
|
2327
|
+
agentName,
|
|
2328
|
+
providerQualifiedModelId,
|
|
2329
|
+
...(result.status === "task_launched" ? { childSessionId: result.childSessionId, asyncMode: true, safeNextActions: ["/flowdesk-status"] } : {}),
|
|
2132
2330
|
resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
|
|
2133
2331
|
resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
|
|
2134
|
-
failureCategory
|
|
2135
|
-
redactedReason
|
|
2136
|
-
summaryForUser:
|
|
2137
|
-
|
|
2138
|
-
|
|
2332
|
+
failureCategory,
|
|
2333
|
+
redactedReason,
|
|
2334
|
+
summaryForUser: taskSummaryForUser({
|
|
2335
|
+
status: result.status,
|
|
2336
|
+
workflowId,
|
|
2337
|
+
laneId,
|
|
2338
|
+
taskId,
|
|
2339
|
+
agentName,
|
|
2340
|
+
providerQualifiedModelId,
|
|
2341
|
+
promptText: taskDescription,
|
|
2342
|
+
...(result.status === "task_completed" ? { resultText: result.resultText } : {}),
|
|
2343
|
+
...(result.status === "task_launched" ? { asyncMode: true } : {}),
|
|
2344
|
+
...(failureCategory === undefined ? {} : { failureCategory }),
|
|
2345
|
+
...(redactedReason === undefined ? {} : { redactedReason }),
|
|
2346
|
+
}),
|
|
2139
2347
|
});
|
|
2140
2348
|
},
|
|
2141
2349
|
}),
|
|
@@ -2317,6 +2525,8 @@ function providerUsageLiveConfigFromOptions(options) {
|
|
|
2317
2525
|
config.geminiProjectId = value.geminiProjectId;
|
|
2318
2526
|
if (value.persistSnapshots === true)
|
|
2319
2527
|
config.persistSnapshots = true;
|
|
2528
|
+
if (value.appendToChat === true)
|
|
2529
|
+
config.appendToChat = true;
|
|
2320
2530
|
const explicitRoot = typeof value.durableStateRootDir === "string" &&
|
|
2321
2531
|
value.durableStateRootDir.trim().length > 0
|
|
2322
2532
|
? value.durableStateRootDir
|
|
@@ -2474,6 +2684,67 @@ function controlledWriteApplyConfigFromOptions(input, options) {
|
|
|
2474
2684
|
const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
|
|
2475
2685
|
return { durableStateRoot, workspaceRoot };
|
|
2476
2686
|
}
|
|
2687
|
+
export const flowdeskOrchestrateToolName = "flowdesk_orchestrate";
|
|
2688
|
+
export const flowdeskOrchestrateOption = "workflowOrchestrate";
|
|
2689
|
+
function orchestrateToolConfigFromOptions(input, options) {
|
|
2690
|
+
const value = options?.[flowdeskOrchestrateOption];
|
|
2691
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
|
|
2692
|
+
return undefined;
|
|
2693
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
|
|
2694
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2695
|
+
if (rootDir === undefined)
|
|
2696
|
+
return undefined;
|
|
2697
|
+
const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2698
|
+
return client === undefined ? undefined : { rootDir, client };
|
|
2699
|
+
}
|
|
2700
|
+
function createFlowDeskOrchestrateOptInTools(config) {
|
|
2701
|
+
return {
|
|
2702
|
+
[flowdeskOrchestrateToolName]: tool({
|
|
2703
|
+
description: [
|
|
2704
|
+
"Run a full FlowDesk multi-task orchestration: Author → Assign → Schedule → Synthesize.",
|
|
2705
|
+
"WHEN TO USE: the user gives a natural-language goal that requires decomposing into multiple tasks, assigning agents/models based on provider usage, executing them in dependency order, and aggregating results.",
|
|
2706
|
+
"INVOKE WITH: goalSummary (clear bounded goal text), parentSessionId, and optional workflowId.",
|
|
2707
|
+
"AFTER CALLING: surface summaryForUser and report synthesisId, taskCount, conflictDetected. All authority flags remain false.",
|
|
2708
|
+
"WHEN NOT TO USE: simple single-task requests, status checks, usage queries, or review fan-out.",
|
|
2709
|
+
].join(" "),
|
|
2710
|
+
args: {
|
|
2711
|
+
goalSummary: tool.schema.string().describe("Bounded natural-language goal for the workflow. Max 500 chars."),
|
|
2712
|
+
parentSessionId: tool.schema.string().describe("Current OpenCode session id (ses-...)."),
|
|
2713
|
+
workflowId: tool.schema.string().optional().describe("Optional stable workflow id. Auto-generated when omitted."),
|
|
2714
|
+
providerQualifiedModelId: tool.schema.string().optional().describe("Concrete provider/model id for author and synthesis calls. Defaults to openai/gpt-5.5."),
|
|
2715
|
+
agentName: tool.schema.string().optional().describe("Agent name for author and synthesis calls. Defaults to reviewer-gpt-frontier."),
|
|
2716
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to allow provider calls."),
|
|
2717
|
+
allowProviderCall: tool.schema.boolean().describe("Must be true to allow provider calls."),
|
|
2718
|
+
allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual lane launch."),
|
|
2719
|
+
},
|
|
2720
|
+
async execute(input) {
|
|
2721
|
+
if (!isRecord(input))
|
|
2722
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "invalid input", safeNextActions: ["/flowdesk-doctor"] });
|
|
2723
|
+
if (input.developerModeAcknowledged !== true || input.allowProviderCall !== true || input.allowActualLaneLaunch !== true)
|
|
2724
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "developerModeAcknowledged, allowProviderCall, and allowActualLaneLaunch must all be true", safeNextActions: ["/flowdesk-doctor"] });
|
|
2725
|
+
const goalSummary = typeof input.goalSummary === "string" ? input.goalSummary.slice(0, 500) : "";
|
|
2726
|
+
if (!goalSummary.trim())
|
|
2727
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "goalSummary is required", safeNextActions: ["/flowdesk-doctor"] });
|
|
2728
|
+
const parentSessionId = typeof input.parentSessionId === "string" ? input.parentSessionId : "";
|
|
2729
|
+
if (!parentSessionId.trim())
|
|
2730
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "parentSessionId is required", safeNextActions: ["/flowdesk-doctor"] });
|
|
2731
|
+
const providerQualifiedModelId = typeof input.providerQualifiedModelId === "string" ? input.providerQualifiedModelId : "openai/gpt-5.5";
|
|
2732
|
+
const agentName = typeof input.agentName === "string" ? input.agentName : "reviewer-gpt-frontier";
|
|
2733
|
+
const workflowId = typeof input.workflowId === "string" ? input.workflowId : undefined;
|
|
2734
|
+
const result = await executeFlowDeskWorkflowOrchestratorV1({
|
|
2735
|
+
workflowId,
|
|
2736
|
+
goalSummary,
|
|
2737
|
+
parentSessionId,
|
|
2738
|
+
rootDir: config.rootDir,
|
|
2739
|
+
client: config.client,
|
|
2740
|
+
providerQualifiedModelId,
|
|
2741
|
+
agentName,
|
|
2742
|
+
});
|
|
2743
|
+
return JSON.stringify(result);
|
|
2744
|
+
},
|
|
2745
|
+
}),
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2477
2748
|
export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
|
|
2478
2749
|
return {
|
|
2479
2750
|
[flowdeskWorkflowDispatchPlanToolName]: tool({
|
|
@@ -2516,6 +2787,33 @@ export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
|
|
|
2516
2787
|
}),
|
|
2517
2788
|
};
|
|
2518
2789
|
}
|
|
2790
|
+
export function createFlowDeskAutoContinuePreviewOptInTools(config) {
|
|
2791
|
+
return {
|
|
2792
|
+
[flowdeskAutoContinuePreviewToolName]: tool({
|
|
2793
|
+
description: [
|
|
2794
|
+
"Preview the next pending task from durable FlowDesk workflow_dispatch_plan evidence without executing it.",
|
|
2795
|
+
"WHEN TO USE: the user asks whether FlowDesk can continue remaining planned work or asks what would run next from a durable plan.",
|
|
2796
|
+
"WHEN NOT TO USE: requests to actually dispatch, call a provider, launch a lane, fallback/reselect, mutate TUI/chat, or write files. This tool is preview-only and never performs continuation.",
|
|
2797
|
+
"INVOKE WITH: workflowId and optional maxSteps. The configured durable state root is used. Todo source is durable workflow_dispatch_plan evidence only, not transient chat/todowrite state.",
|
|
2798
|
+
"AFTER CALLING: surface summaryForUser, nextTaskId/title/summary, pendingTaskCount, and authority. Never claim automatic execution, dispatch, provider call, fallback, hard chat, or TUI action authority.",
|
|
2799
|
+
].join(" "),
|
|
2800
|
+
args: {
|
|
2801
|
+
workflowId: tool.schema.string().describe("Workflow id with durable workflow_dispatch_plan evidence."),
|
|
2802
|
+
maxSteps: tool.schema.number().optional().describe("Bounded preview cap. Clamped to 1..5; no steps are executed."),
|
|
2803
|
+
},
|
|
2804
|
+
async execute(input) {
|
|
2805
|
+
const result = executeFlowDeskAutoContinuePreviewToolV1({
|
|
2806
|
+
config,
|
|
2807
|
+
request: {
|
|
2808
|
+
workflowId: typeof input.workflowId === "string" ? input.workflowId : undefined,
|
|
2809
|
+
maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : undefined,
|
|
2810
|
+
},
|
|
2811
|
+
});
|
|
2812
|
+
return JSON.stringify(result);
|
|
2813
|
+
},
|
|
2814
|
+
}),
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2519
2817
|
export function createFlowDeskWorkflowDispatchOptInTools(config) {
|
|
2520
2818
|
return {
|
|
2521
2819
|
[flowdeskWorkflowDispatchToolName]: tool({
|
|
@@ -2769,6 +3067,134 @@ export function createFlowDeskStatusLiveOptInTools(config) {
|
|
|
2769
3067
|
}),
|
|
2770
3068
|
};
|
|
2771
3069
|
}
|
|
3070
|
+
export function createFlowDeskWorkflowSynthesisPreviewTools(config) {
|
|
3071
|
+
return {
|
|
3072
|
+
[flowdeskWorkflowSynthesisPreviewToolName]: tool({
|
|
3073
|
+
description: [
|
|
3074
|
+
"Build and persist a provider-free FlowDesk synthesis preview from existing task_result evidence. This local tool never calls providers, launches lanes, dispatches runtime work, performs fallback/reselection, writes workspace files, or grants execution authority.",
|
|
3075
|
+
"WHEN TO USE: a workflow reports next_action=synthesis_ready and the user asks to summarize, synthesize, continue with the next synthesis step, or see the completed task results without making a model/provider call.",
|
|
3076
|
+
"WHEN NOT TO USE: requests for model-authored synthesis, real dispatch, provider calls, write/apply, fallback/retry, or replacing raw task results.",
|
|
3077
|
+
"INVOKE WITH: workflowId for a workflow that already has task_result evidence under the configured durable root.",
|
|
3078
|
+
"AFTER CALLING: surface summaryForUser, synthesisId, tasksSummarized, conflictDetected, safeNextActions, and authority. State that this is a provider-free preview derived from durable task_result evidence.",
|
|
3079
|
+
].join(" "),
|
|
3080
|
+
args: {
|
|
3081
|
+
workflowId: tool.schema.string().describe("Workflow id with existing task_result evidence to synthesize locally."),
|
|
3082
|
+
},
|
|
3083
|
+
async execute(input) {
|
|
3084
|
+
const workflowId = isRecord(input) && typeof input.workflowId === "string" ? input.workflowId : "";
|
|
3085
|
+
const result = executeFlowDeskWorkflowSynthesisPreviewV1({ workflowId, rootDir: config.rootDir });
|
|
3086
|
+
return JSON.stringify(result);
|
|
3087
|
+
},
|
|
3088
|
+
}),
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
function isFlowDeskUiProbeEnabled(options) {
|
|
3092
|
+
const raw = options?.[flowdeskUiProbeOption];
|
|
3093
|
+
return process.env.FLOWDESK_UI_PROBE === "1" || raw === true || (isRecord(raw) && raw.enabled === true);
|
|
3094
|
+
}
|
|
3095
|
+
function boundedUiProbeString(value, maxLength = 160) {
|
|
3096
|
+
if (typeof value !== "string" || value.length === 0)
|
|
3097
|
+
return undefined;
|
|
3098
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
|
3099
|
+
}
|
|
3100
|
+
function summarizeFlowDeskUiProbeEventV1(event, observedAt) {
|
|
3101
|
+
const rec = isRecord(event) ? event : {};
|
|
3102
|
+
const properties = Object.keys(rec).sort().slice(0, 20);
|
|
3103
|
+
const nested = (name) => {
|
|
3104
|
+
const value = rec[name];
|
|
3105
|
+
return isRecord(value) ? value : undefined;
|
|
3106
|
+
};
|
|
3107
|
+
const session = nested("session") ?? nested("sessionID") ?? nested("sessionId");
|
|
3108
|
+
const message = nested("message");
|
|
3109
|
+
const part = nested("part");
|
|
3110
|
+
const sessionId = boundedUiProbeString(rec.sessionID) ??
|
|
3111
|
+
boundedUiProbeString(rec.sessionId) ??
|
|
3112
|
+
boundedUiProbeString(session?.id);
|
|
3113
|
+
return {
|
|
3114
|
+
observedAt,
|
|
3115
|
+
eventType: boundedUiProbeString(rec.type, 120) ?? boundedUiProbeString(rec.event, 120) ?? "unknown",
|
|
3116
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
3117
|
+
...(boundedUiProbeString(message?.id) === undefined ? {} : { messageId: boundedUiProbeString(message?.id) }),
|
|
3118
|
+
...(boundedUiProbeString(part?.id) === undefined ? {} : { partId: boundedUiProbeString(part?.id) }),
|
|
3119
|
+
keys: properties,
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function createFlowDeskUiProbeTools(observations) {
|
|
3123
|
+
return {
|
|
3124
|
+
[flowdeskUiProbeToolName]: tool({
|
|
3125
|
+
description: [
|
|
3126
|
+
"Developer-only FlowDesk UI renderer/event probe. Enabled only when FLOWDESK_UI_PROBE=1.",
|
|
3127
|
+
"It emits bounded markdown/details/link/code-fence/long-log samples and reports redacted OpenCode event observations captured by the plugin event hook.",
|
|
3128
|
+
"No dispatch, provider call, write authority, fallback authority, or hard chat control is granted.",
|
|
3129
|
+
].join(" "),
|
|
3130
|
+
args: {
|
|
3131
|
+
mode: tool.schema
|
|
3132
|
+
.string()
|
|
3133
|
+
.optional()
|
|
3134
|
+
.describe("Probe mode: render, events, or all. Defaults to all."),
|
|
3135
|
+
limit: tool.schema
|
|
3136
|
+
.number()
|
|
3137
|
+
.optional()
|
|
3138
|
+
.describe("Maximum recent event observations to return; clamped to 1..50."),
|
|
3139
|
+
},
|
|
3140
|
+
async execute(input) {
|
|
3141
|
+
const mode = isRecord(input) && typeof input.mode === "string" ? input.mode : "all";
|
|
3142
|
+
const requestedLimit = isRecord(input) && typeof input.limit === "number" ? Math.floor(input.limit) : 20;
|
|
3143
|
+
const limit = Math.max(1, Math.min(50, requestedLimit));
|
|
3144
|
+
const includeRender = mode === "all" || mode === "render";
|
|
3145
|
+
const includeEvents = mode === "all" || mode === "events";
|
|
3146
|
+
const longLog = Array.from({ length: 40 }, (_, index) => {
|
|
3147
|
+
const n = String(index + 1).padStart(2, "0");
|
|
3148
|
+
return `${n}. ui-probe timeline event ${n}: message/status/log renderer sample`;
|
|
3149
|
+
});
|
|
3150
|
+
return JSON.stringify({
|
|
3151
|
+
schema_version: "flowdesk.ui_probe_result.v1",
|
|
3152
|
+
status: "ui_probe_collected",
|
|
3153
|
+
probeEnabled: true,
|
|
3154
|
+
mode,
|
|
3155
|
+
authority: {
|
|
3156
|
+
realOpenCodeDispatch: false,
|
|
3157
|
+
providerCall: false,
|
|
3158
|
+
runtimeExecution: false,
|
|
3159
|
+
actualLaneLaunch: false,
|
|
3160
|
+
fallbackAuthority: false,
|
|
3161
|
+
hardCancelOrNoReplyAuthority: false,
|
|
3162
|
+
},
|
|
3163
|
+
...(includeRender
|
|
3164
|
+
? {
|
|
3165
|
+
renderSamples: {
|
|
3166
|
+
markdownDetails: [
|
|
3167
|
+
"<details>",
|
|
3168
|
+
"<summary>FlowDesk UI probe: details summary</summary>",
|
|
3169
|
+
"",
|
|
3170
|
+
"[FlowDesk markdown link probe](https://example.com/flowdesk-ui-probe)",
|
|
3171
|
+
"",
|
|
3172
|
+
"```text",
|
|
3173
|
+
...longLog.slice(0, 8),
|
|
3174
|
+
"```",
|
|
3175
|
+
"</details>",
|
|
3176
|
+
].join("\n"),
|
|
3177
|
+
longLogText: ["FlowDesk UI probe long log", ...longLog].join("\n"),
|
|
3178
|
+
observationInstructions: [
|
|
3179
|
+
"1. Check whether markdownDetails renders as collapsible UI or plain text.",
|
|
3180
|
+
"2. Click the markdown link and re-run mode=events to see whether any click/navigation event reached Hooks.event.",
|
|
3181
|
+
"3. Check whether longLogText is folded, scrollable, or fully expanded by OpenCode's tool-output renderer.",
|
|
3182
|
+
].join("\n"),
|
|
3183
|
+
},
|
|
3184
|
+
}
|
|
3185
|
+
: {}),
|
|
3186
|
+
...(includeEvents
|
|
3187
|
+
? {
|
|
3188
|
+
recentEventObservations: observations.slice(-limit),
|
|
3189
|
+
recentEventObservationCount: Math.min(observations.length, limit),
|
|
3190
|
+
totalBufferedEventObservationCount: observations.length,
|
|
3191
|
+
}
|
|
3192
|
+
: {}),
|
|
3193
|
+
});
|
|
3194
|
+
},
|
|
3195
|
+
}),
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
2772
3198
|
export function createFlowDeskProviderUsageLiveOptInTools(config) {
|
|
2773
3199
|
return {
|
|
2774
3200
|
[flowdeskProviderUsageLiveToolName]: tool({
|
|
@@ -2778,7 +3204,7 @@ export function createFlowDeskProviderUsageLiveOptInTools(config) {
|
|
|
2778
3204
|
"ALSO PROACTIVELY USE: before starting a large multi-step task that depends on a specific provider (e.g. extensive refactor, long agentic loop, multi-perspective review), call this tool first to check whether the chosen provider has enough headroom; if worstAlertLevel is critical or exhausted, warn the user and suggest switching providers or waiting for reset.",
|
|
2779
3205
|
"WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
|
|
2780
3206
|
"INVOKE WITH: optional providerFamily ('claude', 'openai', 'gemini', or 'all'; default 'all'). The plugin user has already opted in to provider-native usage collection at configuration time, so this tool can be called automatically without per-call confirmation.",
|
|
2781
|
-
"AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-
|
|
3207
|
+
"AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-daily, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
|
|
2782
3208
|
].join(" "),
|
|
2783
3209
|
args: {
|
|
2784
3210
|
providerFamily: tool.schema
|
|
@@ -3076,8 +3502,10 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
3076
3502
|
const statusLiveConfig = isStatusLiveEnabled(options)
|
|
3077
3503
|
? statusLiveConfigFromOptions(options)
|
|
3078
3504
|
: undefined;
|
|
3079
|
-
if (statusLiveConfig !== undefined)
|
|
3505
|
+
if (statusLiveConfig !== undefined) {
|
|
3080
3506
|
Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
|
|
3507
|
+
Object.assign(tools, createFlowDeskWorkflowSynthesisPreviewTools({ rootDir: statusLiveConfig.rootDir }));
|
|
3508
|
+
}
|
|
3081
3509
|
const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
|
|
3082
3510
|
? quickFallbackRunConfigFromOptions(options)
|
|
3083
3511
|
: undefined;
|
|
@@ -3093,6 +3521,8 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
3093
3521
|
: undefined;
|
|
3094
3522
|
if (workflowDispatchPlanConfig !== undefined)
|
|
3095
3523
|
Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
|
|
3524
|
+
if (workflowDispatchPlanConfig !== undefined)
|
|
3525
|
+
Object.assign(tools, createFlowDeskAutoContinuePreviewOptInTools(workflowDispatchPlanConfig));
|
|
3096
3526
|
const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
|
|
3097
3527
|
if (workflowDispatchConfig !== undefined)
|
|
3098
3528
|
Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
|
|
@@ -3110,6 +3540,13 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
3110
3540
|
if (agentTaskRunTools !== undefined)
|
|
3111
3541
|
Object.assign(tools, agentTaskRunTools);
|
|
3112
3542
|
}
|
|
3543
|
+
const orchestrateConfig = orchestrateToolConfigFromOptions(input, options);
|
|
3544
|
+
if (orchestrateConfig !== undefined)
|
|
3545
|
+
Object.assign(tools, createFlowDeskOrchestrateOptInTools(orchestrateConfig));
|
|
3546
|
+
const uiProbeEnabled = isFlowDeskUiProbeEnabled(options);
|
|
3547
|
+
const uiProbeEventObservations = [];
|
|
3548
|
+
if (uiProbeEnabled)
|
|
3549
|
+
Object.assign(tools, createFlowDeskUiProbeTools(uiProbeEventObservations));
|
|
3113
3550
|
// P8 Background Watchdog
|
|
3114
3551
|
const watchdogConfig = watchdogConfigFromOptions(options);
|
|
3115
3552
|
const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
|
|
@@ -3210,14 +3647,54 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
3210
3647
|
},
|
|
3211
3648
|
});
|
|
3212
3649
|
}
|
|
3650
|
+
const eventRootDir = durableStateRootFromOptions(options);
|
|
3651
|
+
const eventHook = eventRootDir === undefined && !uiProbeEnabled
|
|
3652
|
+
? undefined
|
|
3653
|
+
: async (input) => {
|
|
3654
|
+
if (uiProbeEnabled) {
|
|
3655
|
+
uiProbeEventObservations.push(summarizeFlowDeskUiProbeEventV1(input.event, new Date().toISOString()));
|
|
3656
|
+
if (uiProbeEventObservations.length > 200)
|
|
3657
|
+
uiProbeEventObservations.splice(0, uiProbeEventObservations.length - 200);
|
|
3658
|
+
}
|
|
3659
|
+
if (eventRootDir !== undefined) {
|
|
3660
|
+
await observeFlowDeskOpenCodeEventV1({ rootDir: eventRootDir, event: input.event });
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3213
3663
|
if (!naturalLanguageRoutingEnabled)
|
|
3214
|
-
return { tool: tools };
|
|
3664
|
+
return eventHook === undefined ? { tool: tools } : { tool: tools, event: eventHook };
|
|
3215
3665
|
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
|
|
3216
3666
|
? input.client
|
|
3217
3667
|
: undefined);
|
|
3668
|
+
// Background sidebar cache refresh interval — runs every 3 minutes regardless of chat activity
|
|
3669
|
+
if (providerUsageLiveConfig?.durableStateRootDir) {
|
|
3670
|
+
const capturedUsageConfig = providerUsageLiveConfig;
|
|
3671
|
+
const sidebarRefreshIntervalMs = 3 * 60_000;
|
|
3672
|
+
const sidebarRefreshInterval = setInterval(() => {
|
|
3673
|
+
const cachePath = join(capturedUsageConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
|
|
3674
|
+
let isStale = false;
|
|
3675
|
+
try {
|
|
3676
|
+
const cache = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
3677
|
+
if (typeof cache.observed_at === "string" && Date.now() - Date.parse(cache.observed_at) > sidebarRefreshIntervalMs)
|
|
3678
|
+
isStale = true;
|
|
3679
|
+
}
|
|
3680
|
+
catch {
|
|
3681
|
+
isStale = true;
|
|
3682
|
+
}
|
|
3683
|
+
if (isStale) {
|
|
3684
|
+
executeFlowDeskProviderUsageLiveV1({
|
|
3685
|
+
config: { ...capturedUsageConfig, persistSidebarCache: true },
|
|
3686
|
+
request: { providerFamily: "all" },
|
|
3687
|
+
}).catch(() => { });
|
|
3688
|
+
}
|
|
3689
|
+
}, sidebarRefreshIntervalMs);
|
|
3690
|
+
sidebarRefreshInterval.unref();
|
|
3691
|
+
process.once("exit", () => clearInterval(sidebarRefreshInterval));
|
|
3692
|
+
process.once("SIGTERM", () => clearInterval(sidebarRefreshInterval));
|
|
3693
|
+
}
|
|
3218
3694
|
return {
|
|
3219
3695
|
tool: tools,
|
|
3220
|
-
|
|
3696
|
+
...(eventHook === undefined ? {} : { event: eventHook }),
|
|
3697
|
+
"chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options), providerUsageLiveConfig),
|
|
3221
3698
|
};
|
|
3222
3699
|
};
|
|
3223
3700
|
export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
|