@xopcai/xopc 0.0.199 → 0.0.200
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/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Bt70bZjv.js → agent-avatar-dicebear-eMarz29u.js} +1 -1
- package/dist/gateway/static/root/assets/agents-DeLW-454.js +68 -0
- package/dist/gateway/static/root/assets/{app-management-settings-panel-B4x7JfL8.js → app-management-settings-panel-yfb68wq9.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-Dwdagf-Q.js → appearance-settings-B8SPknFM.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-DkTiesxe.js → apps-page-Bmor22Ny.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-Cv0VXKLm.js → archive-plugin-DmYRiOP3.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-CgidKrXn.js → attachment-load-D66JvcPi.js} +1 -1
- package/dist/gateway/static/root/assets/automations-page-Q4e8m11h.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-DN3CQgIq.js → binary-plugins-ChIsZArT.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-BwkSl1F_.js → block-editor-Ck-k_d0Z.js} +5 -5
- package/dist/gateway/static/root/assets/browser-settings-page--uVL73Sb.js +8 -0
- package/dist/gateway/static/root/assets/{browser-workflows-page-C_I31wLm.js → browser-workflows-page-BC5jm1zL.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-DXpf3QYN.js +7 -0
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-NAjCsrTi.js +3 -0
- package/dist/gateway/static/root/assets/{channels-settings-DV5BhAeu.js → channels-settings-CiWUo7jW.js} +1 -1
- package/dist/gateway/static/root/assets/{connectors-api-5FEBYguL.js → connectors-api-5xiCRVUS.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-DOwlMseU.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-SEFx5rnr.js → cron-expression-B3PebXKZ.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-D4zCUQJL.js → date-picker-C1Vke7Xn.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-DpQAxS2k.js → dependency-picker-FrQ0tsBC.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-B_5G3lQl.js → desktop-pet-CPBAzNJi.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-ClEyoUJe.js → desktop-pet-settings-D6s3glDP.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-AsFWmOjG.js → directory-picker-path-field-CbNQ7QQS.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-C22Y2o5l.js → extension-debug-page-dTK14hUv.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-C61Ikawk.js → extension-page-Cpdj1Kip.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-WlWWcDFr.js → extension-settings-page-iNHUZGd5.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-api-ByIOMQdc.js → gateway-config-api-SvMvWqMM.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-CzzS9A17.js → gateway-settings-BilR3dGZ.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-zK40dcQ5.js +1 -0
- package/dist/gateway/static/root/assets/home-page-NyXy5MLr.js +3 -0
- package/dist/gateway/static/root/assets/{index-CR233S0Q.css → index-Bp8I_eX4.css} +1 -1
- package/dist/gateway/static/root/assets/index-CCK8zdlS.js +152 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BD_TFz0_.js → keyboard-shortcuts-settings-nKzsroud.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-Ds3sPXTh.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-BwbUeLfm.js → local-apps-page-DV1s_FYh.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-Biz4eA02.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-CW2VpqtT.js → management-settings-D7KjYgHu.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-B1M9KO16.js → markdown-split-CTYbpkrd.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-B_QlNdqZ.js → media-plugins-BXYnaiZ6.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-BTbD4GmH.js → notes-page-CWWVXpw5.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-DnBkQihH.js +4 -0
- package/dist/gateway/static/root/assets/{preview-open-alternatives-CeqtArlZ.js → preview-open-alternatives-pimQAkeI.js} +1 -1
- package/dist/gateway/static/root/assets/{product-open-page-DnalBZZs.js → product-open-page-BoYSS_YR.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-ZO7t1UkB.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-By-vq41g.js +1 -0
- package/dist/gateway/static/root/assets/remote-access-hub-C0C_Y9d0.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-cGkBEfg4.js → runtime-tools-settings-panel-DT8flaX1.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-CmsrLlxu.js → schema-form-DabEP-UI.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-BNlVwUzV.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-CxYBQDaM.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DUWCbaMJ.js → settings-form-section-DCsfZAtC.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-Bvy5rdMY.js → settings-loading-skeleton-Dac6Yd0T.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-cZSAwsRR.js → settings-page-BDoYoEar.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-DWijJ1rw.js +1 -0
- package/dist/gateway/static/root/assets/{share-preview-page-BzA2NCAH.js → share-preview-page-ie9I2_7h.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-DU9VrJLf.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-C5RaIvoE.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-Bvde7_i5.js → skills-page-N0YhaR5S.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-vXSqHR7g.js → skills-page.utils-C639DLXr.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-9cQGOGCK.js → system-settings-panel-CpshpQtC.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-DsTocYkK.js +1 -0
- package/dist/gateway/static/root/assets/{use-autosave-7XfKULPI.js → use-autosave-ms6rDy59.js} +1 -1
- package/dist/gateway/static/root/assets/{user-context-page-CZ9m9aC_.js → user-context-page-BUxIemwy.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-DXr21v-3.js → voice-api-key-field-CObrQWh2.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-CeG7g_DZ.js → work-discovery-overlay-BBxrnEFt.js} +1 -1
- package/dist/gateway/static/root/assets/work-discovery-page-DN3l-HW3.js +1 -0
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-DSQQZP9T.js → workflow-run-setup-panel-BxXhe3IV.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-BCHkjd1i.js +5 -0
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/tasks.js +51 -9
- package/dist/src/agent/context/execution-scope.js +3 -1
- package/dist/src/agent/tasks/task-judge-service.d.ts +1 -1
- package/dist/src/agent/tasks/task-judge-service.js +3 -2
- package/dist/src/agent/tools/xopc-use-tool.js +13 -3
- package/dist/src/automations/domain/validation.d.ts +0 -12
- package/dist/src/automations/draft/automation-draft-validator.d.ts +0 -3
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/gateway/hono/app.js +1 -1
- package/dist/src/gateway/hono/routes/agent-stream.js +2 -74
- package/dist/src/gateway/hono/routes/session-input-handler.d.ts +62 -0
- package/dist/src/gateway/hono/routes/session-input-handler.js +82 -0
- package/dist/src/gateway/hono/routes/tasks.js +161 -2
- package/dist/src/gateway/service/task-conversation-query-service.d.ts +58 -0
- package/dist/src/gateway/service/task-conversation-query-service.js +84 -0
- package/dist/src/gateway/service.d.ts +10 -0
- package/dist/src/gateway/service.js +70 -37
- package/dist/src/projects/project-agent.js +1 -1
- package/dist/src/storage/sqlite/migrations/120_task_conversation.sql +112 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/tasks/execution-context.js +8 -4
- package/dist/src/tasks/index.d.ts +2 -0
- package/dist/src/tasks/index.js +3 -1
- package/dist/src/tasks/task-application-service.js +10 -17
- package/dist/src/tasks/task-context-assembler.js +9 -4
- package/dist/src/tasks/task-context-repository.d.ts +0 -6
- package/dist/src/tasks/task-context-repository.js +0 -4
- package/dist/src/tasks/task-conversation-repository.d.ts +71 -0
- package/dist/src/tasks/task-conversation-repository.js +194 -0
- package/dist/src/tasks/task-handoff-service.d.ts +31 -0
- package/dist/src/tasks/task-handoff-service.js +143 -0
- package/dist/src/tasks/task-read-model-projector.js +1 -5
- package/dist/src/tasks/task-repository.d.ts +0 -2
- package/dist/src/tasks/task-repository.js +2 -9
- package/dist/src/tasks/task-run-coordinator.js +3 -1
- package/dist/src/workflows/service/workflow-run-service.js +4 -2
- package/package.json +3 -3
- package/dist/gateway/static/root/assets/agents-D19uNmJf.js +0 -68
- package/dist/gateway/static/root/assets/automations-page-BR6qMOVr.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-CDFQWAxI.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-D41mssOq.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-BXgCyuu2.js +0 -3
- package/dist/gateway/static/root/assets/connectors-page-C_qkEN0T.js +0 -2
- package/dist/gateway/static/root/assets/heartbeat-settings-B4A_CXUL.js +0 -1
- package/dist/gateway/static/root/assets/home-page-FVGYYPWo.js +0 -3
- package/dist/gateway/static/root/assets/index-CGbKTCST.js +0 -152
- package/dist/gateway/static/root/assets/local-app-workbench-page-BNpkigNb.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-DXP9OQQt.js +0 -2
- package/dist/gateway/static/root/assets/notes-workbench-JRiq7QDW.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-CjWA13aE.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-jsCkScbG.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CGPJTEkM.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-BBHzziMd.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-2cGg8-9h.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-Cf06b66-.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-xlNKydYY.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-C5Xu93c_.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-Bkno4tTe.js +0 -1
- package/dist/gateway/static/root/assets/work-discovery-page-CQWZD03N.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-Cs-MXFJ1.js +0 -5
|
@@ -19,7 +19,7 @@ const log = createLogger('Sqlite:Migrations');
|
|
|
19
19
|
export const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
|
|
20
20
|
|
|
21
21
|
/** Latest schema version this release supports (increment when adding migrations). */
|
|
22
|
-
export const XOPC_DB_SCHEMA_VERSION =
|
|
22
|
+
export const XOPC_DB_SCHEMA_VERSION = 120;
|
|
23
23
|
|
|
24
24
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isXopcDatabaseOpen } from "../storage/sqlite/connection.js";
|
|
2
|
+
import "../storage/sqlite/index.js";
|
|
3
|
+
import { TaskConversationRepository } from "./task-conversation-repository.js";
|
|
1
4
|
//#region src/tasks/execution-context.ts
|
|
2
5
|
const ORIGINS = new Set([
|
|
3
6
|
"chat",
|
|
@@ -17,10 +20,10 @@ const TRIGGERS = new Set([
|
|
|
17
20
|
function optionalString(value) {
|
|
18
21
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
19
22
|
}
|
|
20
|
-
function metadataOrigin(metadata) {
|
|
23
|
+
function metadataOrigin(metadata, taskId) {
|
|
21
24
|
const explicit = optionalString(metadata.customData?.origin);
|
|
22
25
|
if (explicit && ORIGINS.has(explicit)) return explicit;
|
|
23
|
-
if (
|
|
26
|
+
if (taskId) return "task";
|
|
24
27
|
if (metadata.sessionType === "workflow-run" || metadata.sessionType === "workflow-subagent") return "workflow";
|
|
25
28
|
if (metadata.sessionType === "cron") return "automation";
|
|
26
29
|
if (metadata.sessionType === "heartbeat") return "proactive";
|
|
@@ -35,14 +38,15 @@ function metadataTrigger(metadata) {
|
|
|
35
38
|
return "user";
|
|
36
39
|
}
|
|
37
40
|
function resolveExecutionContext(input) {
|
|
41
|
+
const taskId = isXopcDatabaseOpen() ? new TaskConversationRepository().resolveActiveExecutionSession(input.sessionKey)?.taskId : void 0;
|
|
38
42
|
return {
|
|
39
43
|
runId: input.runId,
|
|
40
44
|
sessionKey: input.sessionKey,
|
|
41
45
|
channel: input.channel,
|
|
42
46
|
agentId: input.agentId,
|
|
43
47
|
projectId: input.metadata.projectId,
|
|
44
|
-
taskId
|
|
45
|
-
origin: metadataOrigin(input.metadata),
|
|
48
|
+
taskId,
|
|
49
|
+
origin: metadataOrigin(input.metadata, taskId),
|
|
46
50
|
triggerKind: metadataTrigger(input.metadata),
|
|
47
51
|
parentRunId: optionalString(input.metadata.customData?.parentRunId),
|
|
48
52
|
contextTraceId: optionalString(input.metadata.customData?.contextTraceId),
|
|
@@ -13,6 +13,8 @@ export * from './task-value-metrics-service.js';
|
|
|
13
13
|
export * from './task-application-service.js';
|
|
14
14
|
export * from './task-change-events.js';
|
|
15
15
|
export * from './task-context-repository.js';
|
|
16
|
+
export * from './task-conversation-repository.js';
|
|
17
|
+
export * from './task-handoff-service.js';
|
|
16
18
|
export * from './task-dependency-service.js';
|
|
17
19
|
export * from './task-read-model-projector.js';
|
|
18
20
|
export * from './task-outbox-dispatcher.js';
|
package/dist/src/tasks/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TaskRepository } from "./task-repository.js";
|
|
2
|
+
import { TaskConversationConflictError, TaskConversationRepository } from "./task-conversation-repository.js";
|
|
2
3
|
import { TaskRunRepository } from "./task-run-repository.js";
|
|
3
4
|
import { TaskReadModelProjector } from "./task-read-model-projector.js";
|
|
4
5
|
import { resolveExecutionContext } from "./execution-context.js";
|
|
@@ -16,7 +17,8 @@ import { TaskRunCoordinator } from "./task-run-coordinator.js";
|
|
|
16
17
|
import { ProjectMonitoringService, decideProactiveDisposition, isInQuietHours, nextQuietHoursEnd, proactiveDispositionReason } from "./project-monitoring-service.js";
|
|
17
18
|
import { ProjectOperatingViewService, summarizeProjectOperatingView } from "./project-operating-view-service.js";
|
|
18
19
|
import { TaskValueMetricsService } from "./task-value-metrics-service.js";
|
|
20
|
+
import { TaskHandoffService } from "./task-handoff-service.js";
|
|
19
21
|
import { TaskOutboxDispatcher } from "./task-outbox-dispatcher.js";
|
|
20
22
|
import { TaskRunDispatcher } from "./task-run-dispatcher.js";
|
|
21
23
|
import { TaskSignalService } from "./task-signal-service.js";
|
|
22
|
-
export { AttentionGovernor, HomeQueryService, ModelTaskContractPlanner, ProjectMonitoringService, ProjectOperatingViewService, TaskApplicationService, TaskContextRepository, TaskDependencyError, TaskDependencyService, TaskOutboxDispatcher, TaskReadModelProjector, TaskRepository, TaskRunCoordinator, TaskRunDispatcher, TaskRunRepository, TaskSignalService, TaskValueMetricsService, TaskWorkflowCoordinator, assembleTaskContext, buildHomeBriefing, buildHomeFocusItems, buildTaskExecutionDirective, decideProactiveDisposition, decisionFromTask, defineTaskContract, enqueueTaskChanged, enqueueTaskChangedEvent, getTaskContextManifest, getTaskExecutionBrief, isInQuietHours, nextQuietHoursEnd, parseTaskContractResponse, proactiveDispositionReason, resolveExecutionContext, summarizeProjectOperatingView };
|
|
24
|
+
export { AttentionGovernor, HomeQueryService, ModelTaskContractPlanner, ProjectMonitoringService, ProjectOperatingViewService, TaskApplicationService, TaskContextRepository, TaskConversationConflictError, TaskConversationRepository, TaskDependencyError, TaskDependencyService, TaskHandoffService, TaskOutboxDispatcher, TaskReadModelProjector, TaskRepository, TaskRunCoordinator, TaskRunDispatcher, TaskRunRepository, TaskSignalService, TaskValueMetricsService, TaskWorkflowCoordinator, assembleTaskContext, buildHomeBriefing, buildHomeFocusItems, buildTaskExecutionDirective, decideProactiveDisposition, decisionFromTask, defineTaskContract, enqueueTaskChanged, enqueueTaskChangedEvent, getTaskContextManifest, getTaskExecutionBrief, isInQuietHours, nextQuietHoursEnd, parseTaskContractResponse, proactiveDispositionReason, resolveExecutionContext, summarizeProjectOperatingView };
|
|
@@ -43,7 +43,6 @@ function commandChangedFields(command) {
|
|
|
43
43
|
];
|
|
44
44
|
case "add_wait":
|
|
45
45
|
case "resolve_wait": return ["runs", "attention"];
|
|
46
|
-
case "delegate": return ["delegateAgentId"];
|
|
47
46
|
case "revise_contract": return ["contract"];
|
|
48
47
|
}
|
|
49
48
|
}
|
|
@@ -81,6 +80,10 @@ var TaskApplicationService = class {
|
|
|
81
80
|
...input.contract,
|
|
82
81
|
createdBy: actor
|
|
83
82
|
});
|
|
83
|
+
db.prepare(`INSERT INTO task_conversation_state (
|
|
84
|
+
task_id, active_session_key, current_executor_agent_id,
|
|
85
|
+
assignment_epoch, status, updated_at
|
|
86
|
+
) VALUES (?, NULL, ?, 0, 'idle', ?)`).run(task.id, task.delegateAgentId ?? task.ownerId ?? null, task.createdAt);
|
|
84
87
|
let current = task;
|
|
85
88
|
if (input.dependencies.length > 0) current = this.#dependencies.replace({
|
|
86
89
|
taskId: task.id,
|
|
@@ -128,7 +131,12 @@ var TaskApplicationService = class {
|
|
|
128
131
|
ok: true,
|
|
129
132
|
model: this.#projector.project(current)
|
|
130
133
|
};
|
|
131
|
-
const
|
|
134
|
+
const executor = input.activation.executor ?? (current.delegateAgentId ? {
|
|
135
|
+
kind: "agent",
|
|
136
|
+
agentId: current.delegateAgentId
|
|
137
|
+
} : void 0);
|
|
138
|
+
if (!executor) throw new Error("Task start executor was not resolved");
|
|
139
|
+
const started = this.start(current.id, current.version, executor, {
|
|
132
140
|
idempotencyKey: `${input.idempotencyKey}:start`,
|
|
133
141
|
scheduleAt: input.activation.scheduleAt,
|
|
134
142
|
actor,
|
|
@@ -248,21 +256,6 @@ var TaskApplicationService = class {
|
|
|
248
256
|
}
|
|
249
257
|
break;
|
|
250
258
|
}
|
|
251
|
-
case "delegate": {
|
|
252
|
-
const updated = this.#tasks.update(task.id, {
|
|
253
|
-
expectedVersion: task.version,
|
|
254
|
-
delegateAgentId: input.command.agentId ?? null
|
|
255
|
-
});
|
|
256
|
-
result = updated ? {
|
|
257
|
-
ok: true,
|
|
258
|
-
model: this.#projector.project(updated)
|
|
259
|
-
} : {
|
|
260
|
-
ok: false,
|
|
261
|
-
reason: "conflict",
|
|
262
|
-
model: this.#projector.get(task.id)
|
|
263
|
-
};
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
259
|
case "revise_contract": {
|
|
267
260
|
const updated = this.#tasks.reviseContract({
|
|
268
261
|
taskId: task.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isXopcDatabaseOpen } from "../storage/sqlite/connection.js";
|
|
2
|
-
import { getSessionMetadata } from "../storage/sqlite/session-repository.js";
|
|
3
2
|
import "../storage/sqlite/index.js";
|
|
4
3
|
import { TaskRepository } from "./task-repository.js";
|
|
4
|
+
import { TaskConversationRepository } from "./task-conversation-repository.js";
|
|
5
5
|
import { TaskRunRepository } from "./task-run-repository.js";
|
|
6
6
|
import { TaskContextRepository } from "./task-context-repository.js";
|
|
7
7
|
//#region src/tasks/task-context-assembler.ts
|
|
@@ -12,8 +12,7 @@ const STANDARD = {
|
|
|
12
12
|
reason: "No active task requires expanded context."
|
|
13
13
|
};
|
|
14
14
|
function taskIdForSession(sessionKey) {
|
|
15
|
-
|
|
16
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
15
|
+
return new TaskConversationRepository().resolveActiveExecutionSession(sessionKey)?.taskId;
|
|
17
16
|
}
|
|
18
17
|
function getTaskExecutionBrief(taskId) {
|
|
19
18
|
if (!isXopcDatabaseOpen()) return void 0;
|
|
@@ -87,6 +86,8 @@ function assembleTaskContext(sessionKey, userQuery) {
|
|
|
87
86
|
allocation: STANDARD
|
|
88
87
|
};
|
|
89
88
|
const { task, latestReceipt, remainingCriteria, allocation, manifest } = brief;
|
|
89
|
+
const handoff = new TaskConversationRepository().getLatestHandoff(taskId);
|
|
90
|
+
const handoffPayload = handoff?.toSessionKey === sessionKey ? JSON.stringify(handoff.payload).slice(0, 12e3) : "";
|
|
90
91
|
const contract = task.contract;
|
|
91
92
|
return {
|
|
92
93
|
taskId,
|
|
@@ -97,7 +98,8 @@ function assembleTaskContext(sessionKey, userQuery) {
|
|
|
97
98
|
remainingCriteria.length ? `Remaining acceptance criteria: ${remainingCriteria.join("; ")}` : "",
|
|
98
99
|
contract.constraints.length ? `Constraints: ${contract.constraints.join("; ")}` : "",
|
|
99
100
|
contract.risks.length ? `Risks: ${contract.risks.join("; ")}` : "",
|
|
100
|
-
latestReceipt?.summary ? `Latest run: ${latestReceipt.summary}` : ""
|
|
101
|
+
latestReceipt?.summary ? `Latest run: ${latestReceipt.summary}` : "",
|
|
102
|
+
handoffPayload ? `Handoff snapshot: ${handoffPayload}` : ""
|
|
101
103
|
].filter(Boolean).join("\n"),
|
|
102
104
|
allocation,
|
|
103
105
|
manifest
|
|
@@ -108,6 +110,8 @@ function buildTaskExecutionDirective(sessionKey) {
|
|
|
108
110
|
const brief = taskId ? getTaskExecutionBrief(taskId) : void 0;
|
|
109
111
|
if (!brief) return "";
|
|
110
112
|
const { task, remainingCriteria } = brief;
|
|
113
|
+
const handoff = new TaskConversationRepository().getLatestHandoff(task.id);
|
|
114
|
+
const handoffPayload = handoff?.toSessionKey === sessionKey ? JSON.stringify(handoff.payload).slice(0, 12e3) : "";
|
|
111
115
|
const contract = task.contract;
|
|
112
116
|
return [
|
|
113
117
|
"<xopc_task_execution>",
|
|
@@ -117,6 +121,7 @@ function buildTaskExecutionDirective(sessionKey) {
|
|
|
117
121
|
`Remaining acceptance criteria: ${remainingCriteria.join("; ")}`,
|
|
118
122
|
contract.constraints.length ? `Constraints: ${contract.constraints.join("; ")}` : "",
|
|
119
123
|
contract.approvalRequired.length ? `Authority required: ${contract.approvalRequired.join("; ")}` : "",
|
|
124
|
+
handoffPayload ? `Handoff snapshot: ${handoffPayload}` : "",
|
|
120
125
|
"Take safe in-scope steps and produce inspectable evidence. Do not claim completion without verification.",
|
|
121
126
|
"</xopc_task_execution>"
|
|
122
127
|
].filter(Boolean).join("\n");
|
|
@@ -42,11 +42,5 @@ export declare class TaskContextRepository {
|
|
|
42
42
|
}): TaskAuthorityGrant;
|
|
43
43
|
listActiveGrants(taskId: string, now?: number): TaskAuthorityGrant[];
|
|
44
44
|
revoke(grantId: string, now?: number): boolean;
|
|
45
|
-
linkSession(input: {
|
|
46
|
-
taskId: string;
|
|
47
|
-
sessionKey: string;
|
|
48
|
-
role: 'primary' | 'discussion' | 'execution';
|
|
49
|
-
now?: number;
|
|
50
|
-
}): void;
|
|
51
45
|
private requireGrant;
|
|
52
46
|
}
|
|
@@ -82,10 +82,6 @@ var TaskContextRepository = class {
|
|
|
82
82
|
revoke(grantId, now = Date.now()) {
|
|
83
83
|
return getSqliteDatabase().prepare(`UPDATE task_authority_grants SET revoked_at = ?
|
|
84
84
|
WHERE grant_id = ? AND revoked_at IS NULL`).run(now, grantId).changes > 0;
|
|
85
|
-
}
|
|
86
|
-
linkSession(input) {
|
|
87
|
-
getSqliteDatabase().prepare(`INSERT OR IGNORE INTO task_sessions (task_session_id, task_id, session_key, role, created_at)
|
|
88
|
-
VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), input.taskId, input.sessionKey, input.role, input.now ?? Date.now());
|
|
89
85
|
}
|
|
90
86
|
requireGrant(id) {
|
|
91
87
|
const row = getSqliteDatabase().prepare("SELECT * FROM task_authority_grants WHERE grant_id = ?").get(id);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type TaskConversationStatus = 'idle' | 'active';
|
|
2
|
+
export type TaskSessionRole = 'primary' | 'discussion' | 'execution';
|
|
3
|
+
export type TaskSessionStatus = 'active' | 'completed' | 'superseded' | 'failed';
|
|
4
|
+
export interface TaskConversationState {
|
|
5
|
+
taskId: string;
|
|
6
|
+
activeSessionKey?: string;
|
|
7
|
+
currentExecutorAgentId?: string;
|
|
8
|
+
assignmentEpoch: number;
|
|
9
|
+
status: TaskConversationStatus;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TaskSessionLink {
|
|
13
|
+
id: string;
|
|
14
|
+
taskId: string;
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
role: TaskSessionRole;
|
|
17
|
+
agentId?: string;
|
|
18
|
+
runId?: string;
|
|
19
|
+
assignmentEpoch: number;
|
|
20
|
+
status: TaskSessionStatus;
|
|
21
|
+
startedAt: number;
|
|
22
|
+
endedAt?: number;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface TaskHandoffSnapshot {
|
|
26
|
+
id: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
fromSessionKey?: string;
|
|
29
|
+
toSessionKey: string;
|
|
30
|
+
fromAgentId?: string;
|
|
31
|
+
toAgentId: string;
|
|
32
|
+
assignmentEpoch: number;
|
|
33
|
+
payload: Record<string, unknown>;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
export declare class TaskConversationConflictError extends Error {
|
|
37
|
+
}
|
|
38
|
+
export declare class TaskConversationRepository {
|
|
39
|
+
findHandoffByIdempotencyKey(input: {
|
|
40
|
+
taskId: string;
|
|
41
|
+
expectedTaskVersion: number;
|
|
42
|
+
toAgentId: string;
|
|
43
|
+
idempotencyKey: string;
|
|
44
|
+
}): TaskHandoffSnapshot | undefined;
|
|
45
|
+
getState(taskId: string): TaskConversationState | undefined;
|
|
46
|
+
requireState(taskId: string): TaskConversationState;
|
|
47
|
+
listSessions(taskId: string): TaskSessionLink[];
|
|
48
|
+
getActiveSession(taskId: string): TaskSessionLink | undefined;
|
|
49
|
+
resolveActiveExecutionSession(sessionKey: string): TaskSessionLink | undefined;
|
|
50
|
+
activateExecutionSession(input: {
|
|
51
|
+
taskId: string;
|
|
52
|
+
sessionKey: string;
|
|
53
|
+
agentId: string;
|
|
54
|
+
runId?: string;
|
|
55
|
+
now?: number;
|
|
56
|
+
}): TaskConversationState;
|
|
57
|
+
completeHandoff(input: {
|
|
58
|
+
taskId: string;
|
|
59
|
+
expectedTaskVersion: number;
|
|
60
|
+
toSessionKey: string;
|
|
61
|
+
toAgentId: string;
|
|
62
|
+
idempotencyKey: string;
|
|
63
|
+
payload: Record<string, unknown>;
|
|
64
|
+
now?: number;
|
|
65
|
+
}): {
|
|
66
|
+
state: TaskConversationState;
|
|
67
|
+
snapshot: TaskHandoffSnapshot;
|
|
68
|
+
taskVersion: number;
|
|
69
|
+
};
|
|
70
|
+
getLatestHandoff(taskId: string): TaskHandoffSnapshot | undefined;
|
|
71
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
//#region src/tasks/task-conversation-repository.ts
|
|
4
|
+
var TaskConversationConflictError = class extends Error {};
|
|
5
|
+
function stateFromRow(row) {
|
|
6
|
+
return {
|
|
7
|
+
taskId: row.task_id,
|
|
8
|
+
...row.active_session_key ? { activeSessionKey: row.active_session_key } : {},
|
|
9
|
+
...row.current_executor_agent_id ? { currentExecutorAgentId: row.current_executor_agent_id } : {},
|
|
10
|
+
assignmentEpoch: row.assignment_epoch,
|
|
11
|
+
status: row.status,
|
|
12
|
+
updatedAt: row.updated_at
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function linkFromRow(row) {
|
|
16
|
+
if (!row.session_key) return void 0;
|
|
17
|
+
return {
|
|
18
|
+
id: row.task_session_id,
|
|
19
|
+
taskId: row.task_id,
|
|
20
|
+
sessionKey: row.session_key,
|
|
21
|
+
role: row.role,
|
|
22
|
+
...row.agent_id ? { agentId: row.agent_id } : {},
|
|
23
|
+
...row.run_id ? { runId: row.run_id } : {},
|
|
24
|
+
assignmentEpoch: row.assignment_epoch,
|
|
25
|
+
status: row.status,
|
|
26
|
+
startedAt: row.started_at ?? row.created_at,
|
|
27
|
+
...row.ended_at === null ? {} : { endedAt: row.ended_at },
|
|
28
|
+
createdAt: row.created_at
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function snapshotFromRow(row) {
|
|
32
|
+
return {
|
|
33
|
+
id: row.snapshot_id,
|
|
34
|
+
taskId: row.task_id,
|
|
35
|
+
...row.from_session_key ? { fromSessionKey: row.from_session_key } : {},
|
|
36
|
+
toSessionKey: row.to_session_key,
|
|
37
|
+
...row.from_agent_id ? { fromAgentId: row.from_agent_id } : {},
|
|
38
|
+
toAgentId: row.to_agent_id,
|
|
39
|
+
assignmentEpoch: row.assignment_epoch,
|
|
40
|
+
payload: JSON.parse(row.payload_json),
|
|
41
|
+
createdAt: row.created_at
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function handoffRequestHash(input) {
|
|
45
|
+
return createHash("sha256").update(JSON.stringify(input)).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
var TaskConversationRepository = class {
|
|
48
|
+
findHandoffByIdempotencyKey(input) {
|
|
49
|
+
const row = getSqliteDatabase().prepare(`SELECT request_hash, result_json FROM command_deduplication
|
|
50
|
+
WHERE idempotency_key = ?`).get(input.idempotencyKey);
|
|
51
|
+
if (!row) return void 0;
|
|
52
|
+
const requestHash = handoffRequestHash(input);
|
|
53
|
+
if (row.request_hash !== requestHash) throw new TaskConversationConflictError("Idempotency key was reused with different input");
|
|
54
|
+
const result = JSON.parse(row.result_json);
|
|
55
|
+
const snapshot = getSqliteDatabase().prepare("SELECT * FROM task_handoff_snapshots WHERE snapshot_id = ?").get(result.snapshotId);
|
|
56
|
+
if (!snapshot || snapshot.task_id !== input.taskId) throw new Error(`Task handoff snapshot not found: ${result.snapshotId}`);
|
|
57
|
+
return snapshotFromRow(snapshot);
|
|
58
|
+
}
|
|
59
|
+
getState(taskId) {
|
|
60
|
+
const row = getSqliteDatabase().prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(taskId);
|
|
61
|
+
return row ? stateFromRow(row) : void 0;
|
|
62
|
+
}
|
|
63
|
+
requireState(taskId) {
|
|
64
|
+
const state = this.getState(taskId);
|
|
65
|
+
if (!state) throw new Error(`Task conversation state not found: ${taskId}`);
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
listSessions(taskId) {
|
|
69
|
+
return getSqliteDatabase().prepare(`SELECT * FROM task_sessions WHERE task_id = ? AND session_key IS NOT NULL
|
|
70
|
+
ORDER BY assignment_epoch DESC, created_at DESC, task_session_id DESC`).all(taskId).flatMap((row) => {
|
|
71
|
+
const link = linkFromRow(row);
|
|
72
|
+
return link ? [link] : [];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
getActiveSession(taskId) {
|
|
76
|
+
const row = getSqliteDatabase().prepare(`SELECT * FROM task_sessions
|
|
77
|
+
WHERE task_id = ? AND role = 'execution' AND status = 'active'
|
|
78
|
+
LIMIT 1`).get(taskId);
|
|
79
|
+
return row ? linkFromRow(row) : void 0;
|
|
80
|
+
}
|
|
81
|
+
resolveActiveExecutionSession(sessionKey) {
|
|
82
|
+
const row = getSqliteDatabase().prepare(`SELECT * FROM task_sessions
|
|
83
|
+
WHERE session_key = ? AND role = 'execution' AND status = 'active'
|
|
84
|
+
LIMIT 1`).get(sessionKey);
|
|
85
|
+
return row ? linkFromRow(row) : void 0;
|
|
86
|
+
}
|
|
87
|
+
activateExecutionSession(input) {
|
|
88
|
+
const now = input.now ?? Date.now();
|
|
89
|
+
return runSqliteWriteTransaction((db) => {
|
|
90
|
+
const existing = db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId);
|
|
91
|
+
const current = db.prepare(`SELECT * FROM task_sessions
|
|
92
|
+
WHERE task_id = ? AND role = 'execution' AND status = 'active'`).get(input.taskId);
|
|
93
|
+
const sameSession = current?.session_key === input.sessionKey;
|
|
94
|
+
const assignmentEpoch = sameSession ? current.assignment_epoch : (existing?.assignment_epoch ?? 0) + 1;
|
|
95
|
+
if (!sameSession) db.prepare(`UPDATE task_sessions SET status = 'superseded', ended_at = ?
|
|
96
|
+
WHERE task_id = ? AND role = 'execution' AND status = 'active'`).run(now, input.taskId);
|
|
97
|
+
db.prepare(`INSERT INTO task_sessions (
|
|
98
|
+
task_session_id, task_id, session_key, role, created_at,
|
|
99
|
+
agent_id, run_id, assignment_epoch, status, started_at, ended_at
|
|
100
|
+
) VALUES (?, ?, ?, 'execution', ?, ?, ?, ?, 'active', ?, NULL)
|
|
101
|
+
ON CONFLICT(task_id, session_key, role) WHERE session_key IS NOT NULL DO UPDATE SET
|
|
102
|
+
agent_id = excluded.agent_id,
|
|
103
|
+
run_id = COALESCE(excluded.run_id, task_sessions.run_id),
|
|
104
|
+
assignment_epoch = excluded.assignment_epoch,
|
|
105
|
+
status = 'active',
|
|
106
|
+
started_at = excluded.started_at,
|
|
107
|
+
ended_at = NULL`).run(randomUUID(), input.taskId, input.sessionKey, now, input.agentId, input.runId ?? null, assignmentEpoch, sameSession ? current.started_at ?? now : now);
|
|
108
|
+
db.prepare(`INSERT INTO task_conversation_state (
|
|
109
|
+
task_id, active_session_key, current_executor_agent_id,
|
|
110
|
+
assignment_epoch, status, updated_at
|
|
111
|
+
) VALUES (?, ?, ?, ?, 'active', ?)
|
|
112
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
113
|
+
active_session_key = excluded.active_session_key,
|
|
114
|
+
current_executor_agent_id = excluded.current_executor_agent_id,
|
|
115
|
+
assignment_epoch = excluded.assignment_epoch,
|
|
116
|
+
status = 'active',
|
|
117
|
+
updated_at = excluded.updated_at`).run(input.taskId, input.sessionKey, input.agentId, assignmentEpoch, now);
|
|
118
|
+
return stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
completeHandoff(input) {
|
|
122
|
+
const now = input.now ?? Date.now();
|
|
123
|
+
return runSqliteWriteTransaction((db) => {
|
|
124
|
+
const requestHash = handoffRequestHash({
|
|
125
|
+
taskId: input.taskId,
|
|
126
|
+
expectedTaskVersion: input.expectedTaskVersion,
|
|
127
|
+
toAgentId: input.toAgentId
|
|
128
|
+
});
|
|
129
|
+
const duplicate = db.prepare(`SELECT request_hash, result_json FROM command_deduplication
|
|
130
|
+
WHERE idempotency_key = ?`).get(input.idempotencyKey);
|
|
131
|
+
if (duplicate) {
|
|
132
|
+
if (duplicate.request_hash !== requestHash) throw new TaskConversationConflictError("Idempotency key was reused with different input");
|
|
133
|
+
const result = JSON.parse(duplicate.result_json);
|
|
134
|
+
const snapshotRow = db.prepare("SELECT * FROM task_handoff_snapshots WHERE snapshot_id = ?").get(result.snapshotId);
|
|
135
|
+
const taskRow = db.prepare("SELECT version FROM tasks WHERE task_id = ?").get(input.taskId);
|
|
136
|
+
return {
|
|
137
|
+
state: stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId)),
|
|
138
|
+
snapshot: snapshotFromRow(snapshotRow),
|
|
139
|
+
taskVersion: taskRow.version
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const existing = db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId);
|
|
143
|
+
if (!existing) throw new Error(`Task conversation state not found: ${input.taskId}`);
|
|
144
|
+
const current = db.prepare(`SELECT * FROM task_sessions
|
|
145
|
+
WHERE task_id = ? AND role = 'execution' AND status = 'active'`).get(input.taskId);
|
|
146
|
+
if (current?.session_key === input.toSessionKey && current.agent_id === input.toAgentId) throw new TaskConversationConflictError("Executor is already active");
|
|
147
|
+
const assignmentEpoch = existing.assignment_epoch + 1;
|
|
148
|
+
if (db.prepare(`UPDATE tasks SET delegate_agent_id = ?, version = version + 1, updated_at = ?
|
|
149
|
+
WHERE task_id = ? AND version = ?`).run(input.toAgentId, now, input.taskId, input.expectedTaskVersion).changes === 0) throw new TaskConversationConflictError("Task changed");
|
|
150
|
+
db.prepare(`UPDATE task_sessions SET status = 'superseded', ended_at = ?
|
|
151
|
+
WHERE task_id = ? AND role = 'execution' AND status = 'active'`).run(now, input.taskId);
|
|
152
|
+
db.prepare(`INSERT INTO task_sessions (
|
|
153
|
+
task_session_id, task_id, session_key, role, created_at,
|
|
154
|
+
agent_id, assignment_epoch, status, started_at, ended_at
|
|
155
|
+
) VALUES (?, ?, ?, 'execution', ?, ?, ?, 'active', ?, NULL)`).run(randomUUID(), input.taskId, input.toSessionKey, now, input.toAgentId, assignmentEpoch, now);
|
|
156
|
+
const snapshotId = randomUUID();
|
|
157
|
+
db.prepare(`INSERT INTO task_handoff_snapshots (
|
|
158
|
+
snapshot_id, task_id, from_session_key, to_session_key,
|
|
159
|
+
from_agent_id, to_agent_id, assignment_epoch, payload_json, created_at
|
|
160
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(snapshotId, input.taskId, current?.session_key ?? null, input.toSessionKey, current?.agent_id ?? existing.current_executor_agent_id ?? null, input.toAgentId, assignmentEpoch, JSON.stringify(input.payload), now);
|
|
161
|
+
db.prepare(`UPDATE task_conversation_state SET
|
|
162
|
+
active_session_key = ?, current_executor_agent_id = ?,
|
|
163
|
+
assignment_epoch = ?, status = 'active', updated_at = ?
|
|
164
|
+
WHERE task_id = ?`).run(input.toSessionKey, input.toAgentId, assignmentEpoch, now, input.taskId);
|
|
165
|
+
db.prepare(`INSERT INTO command_deduplication (
|
|
166
|
+
idempotency_key, command_type, subject_kind, subject_id,
|
|
167
|
+
request_hash, result_json, created_at
|
|
168
|
+
) VALUES (?, 'task.handoff', 'task', ?, ?, ?, ?)`).run(input.idempotencyKey, input.taskId, requestHash, JSON.stringify({ snapshotId }), now);
|
|
169
|
+
return {
|
|
170
|
+
state: stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId)),
|
|
171
|
+
taskVersion: input.expectedTaskVersion + 1,
|
|
172
|
+
snapshot: {
|
|
173
|
+
id: snapshotId,
|
|
174
|
+
taskId: input.taskId,
|
|
175
|
+
...current?.session_key ? { fromSessionKey: current.session_key } : {},
|
|
176
|
+
toSessionKey: input.toSessionKey,
|
|
177
|
+
...current?.agent_id || existing.current_executor_agent_id ? { fromAgentId: current?.agent_id ?? existing.current_executor_agent_id } : {},
|
|
178
|
+
toAgentId: input.toAgentId,
|
|
179
|
+
assignmentEpoch,
|
|
180
|
+
payload: input.payload,
|
|
181
|
+
createdAt: now
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
getLatestHandoff(taskId) {
|
|
187
|
+
const row = getSqliteDatabase().prepare(`SELECT * FROM task_handoff_snapshots
|
|
188
|
+
WHERE task_id = ? ORDER BY assignment_epoch DESC LIMIT 1`).get(taskId);
|
|
189
|
+
if (!row) return void 0;
|
|
190
|
+
return snapshotFromRow(row);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
//#endregion
|
|
194
|
+
export { TaskConversationConflictError, TaskConversationRepository };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Config } from '../config/schema.js';
|
|
2
|
+
import type { SessionIndex } from '../session/index.js';
|
|
3
|
+
import { type TaskConversationState, type TaskHandoffSnapshot } from './task-conversation-repository.js';
|
|
4
|
+
import { type TaskAggregate } from './task-repository.js';
|
|
5
|
+
export interface TaskHandoffResult {
|
|
6
|
+
task: TaskAggregate;
|
|
7
|
+
conversation: TaskConversationState;
|
|
8
|
+
snapshot: TaskHandoffSnapshot;
|
|
9
|
+
fromAgentId?: string;
|
|
10
|
+
toAgentId: string;
|
|
11
|
+
activeSessionKey: string;
|
|
12
|
+
assignmentEpoch: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class TaskHandoffService {
|
|
15
|
+
#private;
|
|
16
|
+
private readonly deps;
|
|
17
|
+
constructor(deps: {
|
|
18
|
+
getConfig: () => Config;
|
|
19
|
+
sessionIndex: SessionIndex;
|
|
20
|
+
getActiveRunId: (sessionKey: string) => string | undefined;
|
|
21
|
+
abortRun: (runId: string) => Promise<unknown>;
|
|
22
|
+
});
|
|
23
|
+
handoff(input: {
|
|
24
|
+
taskId: string;
|
|
25
|
+
toAgentId: string;
|
|
26
|
+
expectedVersion: number;
|
|
27
|
+
idempotencyKey: string;
|
|
28
|
+
}): Promise<TaskHandoffResult>;
|
|
29
|
+
private performHandoff;
|
|
30
|
+
private abortPreviousSession;
|
|
31
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
2
|
+
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { init_agent_scope, listAgentEntries, normalizeAgentId } from "../agent/agent-scope.js";
|
|
4
|
+
import { buildSessionKey, init_session_key, sanitizeSegment } from "../routing/session-key.js";
|
|
5
|
+
import { TaskRepository } from "./task-repository.js";
|
|
6
|
+
import { TaskConversationRepository } from "./task-conversation-repository.js";
|
|
7
|
+
import { TaskRunRepository } from "./task-run-repository.js";
|
|
8
|
+
import { TaskContextRepository } from "./task-context-repository.js";
|
|
9
|
+
//#region src/tasks/task-handoff-service.ts
|
|
10
|
+
init_agent_scope();
|
|
11
|
+
init_session_key();
|
|
12
|
+
init_logger();
|
|
13
|
+
const log = createLogger("TaskHandoff");
|
|
14
|
+
var TaskHandoffService = class {
|
|
15
|
+
#inflight = /* @__PURE__ */ new Map();
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
}
|
|
19
|
+
handoff(input) {
|
|
20
|
+
const previous = this.#inflight.get(input.taskId);
|
|
21
|
+
const pending = (previous ? previous.catch(() => void 0) : Promise.resolve()).then(() => this.performHandoff(input));
|
|
22
|
+
this.#inflight.set(input.taskId, pending);
|
|
23
|
+
pending.finally(() => {
|
|
24
|
+
if (this.#inflight.get(input.taskId) === pending) this.#inflight.delete(input.taskId);
|
|
25
|
+
});
|
|
26
|
+
return pending;
|
|
27
|
+
}
|
|
28
|
+
async performHandoff(input) {
|
|
29
|
+
const config = this.deps.getConfig();
|
|
30
|
+
const toAgentId = normalizeAgentId(input.toAgentId);
|
|
31
|
+
if (!listAgentEntries(config).find((entry) => entry.enabled !== false && normalizeAgentId(entry.id) === toAgentId)) throw new Error(`Agent not found: ${toAgentId}`);
|
|
32
|
+
const tasks = new TaskRepository();
|
|
33
|
+
const task = tasks.require(input.taskId);
|
|
34
|
+
const conversations = new TaskConversationRepository();
|
|
35
|
+
const duplicate = conversations.findHandoffByIdempotencyKey({
|
|
36
|
+
taskId: task.id,
|
|
37
|
+
expectedTaskVersion: input.expectedVersion,
|
|
38
|
+
toAgentId,
|
|
39
|
+
idempotencyKey: input.idempotencyKey
|
|
40
|
+
});
|
|
41
|
+
if (duplicate) {
|
|
42
|
+
await this.abortPreviousSession(duplicate.fromSessionKey);
|
|
43
|
+
return {
|
|
44
|
+
task,
|
|
45
|
+
conversation: conversations.requireState(task.id),
|
|
46
|
+
snapshot: duplicate,
|
|
47
|
+
...duplicate.fromAgentId ? { fromAgentId: duplicate.fromAgentId } : {},
|
|
48
|
+
toAgentId,
|
|
49
|
+
activeSessionKey: duplicate.toSessionKey,
|
|
50
|
+
assignmentEpoch: duplicate.assignmentEpoch
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (task.version !== input.expectedVersion) throw new Error("Task changed");
|
|
54
|
+
const previousState = conversations.requireState(task.id);
|
|
55
|
+
if (previousState.currentExecutorAgentId === toAgentId) throw new Error("Executor is already active");
|
|
56
|
+
const assignmentEpoch = previousState.assignmentEpoch + 1;
|
|
57
|
+
const peerId = sanitizeSegment(`task-${task.id}-assignment-${assignmentEpoch}`);
|
|
58
|
+
const sessionKey = buildSessionKey({
|
|
59
|
+
agentId: toAgentId,
|
|
60
|
+
source: "webchat",
|
|
61
|
+
accountId: "default",
|
|
62
|
+
peerKind: "direct",
|
|
63
|
+
peerId
|
|
64
|
+
});
|
|
65
|
+
const runs = new TaskRunRepository();
|
|
66
|
+
const latestRun = runs.getLatestRoot(task.id);
|
|
67
|
+
const latestReceipt = latestRun ? runs.getReceipt(latestRun.id) : void 0;
|
|
68
|
+
const remainingCriteria = latestReceipt?.verification.checks.length ? latestReceipt.verification.checks.filter((check) => check.status !== "passed").map((check) => check.criterion) : task.contract?.acceptanceCriteria ?? [];
|
|
69
|
+
const payload = {
|
|
70
|
+
objective: task.contract?.objective ?? task.title,
|
|
71
|
+
expectedOutputs: task.contract?.expectedOutputs ?? [],
|
|
72
|
+
remainingCriteria,
|
|
73
|
+
constraints: task.contract?.constraints ?? [],
|
|
74
|
+
risks: task.contract?.risks ?? [],
|
|
75
|
+
approvalRequired: task.contract?.approvalRequired ?? [],
|
|
76
|
+
context: new TaskContextRepository().list(task.id),
|
|
77
|
+
...latestReceipt ? {
|
|
78
|
+
latestReceipt,
|
|
79
|
+
completedWork: latestReceipt.changes,
|
|
80
|
+
remainingWork: latestReceipt.remainingWork
|
|
81
|
+
} : {}
|
|
82
|
+
};
|
|
83
|
+
await this.deps.sessionIndex.saveMessages(sessionKey, [], { metadata: {
|
|
84
|
+
sourceChannel: "webchat",
|
|
85
|
+
sourceChatId: `default:direct:${peerId}`,
|
|
86
|
+
sessionType: "chat",
|
|
87
|
+
routing: {
|
|
88
|
+
agentId: toAgentId,
|
|
89
|
+
source: "webchat",
|
|
90
|
+
accountId: "default",
|
|
91
|
+
peerKind: "direct",
|
|
92
|
+
peerId
|
|
93
|
+
},
|
|
94
|
+
projectId: task.projectId,
|
|
95
|
+
customData: {
|
|
96
|
+
origin: "task",
|
|
97
|
+
triggerKind: "user",
|
|
98
|
+
assignmentEpoch
|
|
99
|
+
}
|
|
100
|
+
} });
|
|
101
|
+
let completed;
|
|
102
|
+
try {
|
|
103
|
+
completed = conversations.completeHandoff({
|
|
104
|
+
taskId: task.id,
|
|
105
|
+
expectedTaskVersion: input.expectedVersion,
|
|
106
|
+
toSessionKey: sessionKey,
|
|
107
|
+
toAgentId,
|
|
108
|
+
idempotencyKey: input.idempotencyKey,
|
|
109
|
+
payload
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
await this.deps.sessionIndex.delete(sessionKey);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const oldSessionKey = completed.snapshot.fromSessionKey;
|
|
116
|
+
await this.abortPreviousSession(oldSessionKey);
|
|
117
|
+
return {
|
|
118
|
+
task: tasks.require(task.id),
|
|
119
|
+
conversation: completed.state,
|
|
120
|
+
snapshot: completed.snapshot,
|
|
121
|
+
...completed.snapshot.fromAgentId ? { fromAgentId: completed.snapshot.fromAgentId } : {},
|
|
122
|
+
toAgentId,
|
|
123
|
+
activeSessionKey: sessionKey,
|
|
124
|
+
assignmentEpoch: completed.state.assignmentEpoch
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async abortPreviousSession(sessionKey) {
|
|
128
|
+
if (!sessionKey) return;
|
|
129
|
+
const activeRunId = this.deps.getActiveRunId(sessionKey);
|
|
130
|
+
if (!activeRunId) return;
|
|
131
|
+
try {
|
|
132
|
+
await this.deps.abortRun(activeRunId);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
log.warn({
|
|
135
|
+
err: error,
|
|
136
|
+
sessionKey,
|
|
137
|
+
runId: activeRunId
|
|
138
|
+
}, "Previous task executor did not stop cleanly");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
export { TaskHandoffService };
|