@tomflow/proflow-platform-host 0.1.30 → 0.1.31
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/CHANGELOG.md +6 -0
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/adapter.js +22 -14
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +437 -15
- package/dist/src/monitor-rotation-coordinator.d.ts +76 -0
- package/dist/src/monitor-rotation-coordinator.js +782 -0
- package/dist/src/product-campaign-continuation.d.ts +132 -0
- package/dist/src/product-campaign-continuation.js +279 -0
- package/dist/src/product-discussion-admission.d.ts +92 -0
- package/dist/src/product-discussion-admission.js +191 -0
- package/dist/src/product-discussion-host.d.ts +43 -0
- package/dist/src/product-discussion-host.js +167 -0
- package/dist/src/reconciliation-coordinator.d.ts +1 -0
- package/dist/src/reconciliation-coordinator.js +7 -2
- package/dist/src/role-operations.js +8 -0
- package/package.json +9 -9
- package/proflow.module.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { chmod, mkdir, readFile, stat, writeFile, } from "node:fs/promises";
|
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { createAgentRuntime } from "@tomflow/proflow-agent-runtime";
|
|
8
|
+
import { createMonitorControlClient, } from "@tomflow/proflow-execution-browser-extension";
|
|
8
9
|
import { createLocalToolBridgeHostClient, LocalToolBridgeError, } from "@tomflow/proflow-execution-browser-extension/local-tool-bridge";
|
|
9
10
|
import { applyMigrations } from "@tomflow/proflow-task-migration-runner";
|
|
10
11
|
import { createTaskServices, publicOperationNames, } from "@tomflow/proflow-task-orchestration";
|
|
@@ -13,10 +14,23 @@ import { taskMigrations } from "@tomflow/proflow-task-store-sqlite/migrations";
|
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import { resolveBrowserPermissionTaskBinding } from "./browser-permission-context.js";
|
|
15
16
|
import { classifyBrowserPermission, } from "./browser-permission-policy.js";
|
|
17
|
+
import { authorizeMonitorExecution, createMonitorRotationCoordinator, } from "./monitor-rotation-coordinator.js";
|
|
18
|
+
import { canonicalizeProductOperation, createProductCampaignContinuationCoordinator, } from "./product-campaign-continuation.js";
|
|
19
|
+
import { createProductDiscussionHost } from "./product-discussion-host.js";
|
|
16
20
|
import { createReconciliationCoordinator } from "./reconciliation-coordinator.js";
|
|
17
21
|
import { directToolActionIds, roleAllowsDirectToolOperation, roleOperations, rolePackageRefs, } from "./role-operations.js";
|
|
18
22
|
const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
19
23
|
const OWNER_INVOKE_WATCHDOG_MS = 120_000;
|
|
24
|
+
const productCampaignOperationIds = new Set([
|
|
25
|
+
"getProductCampaign",
|
|
26
|
+
"getProductDiscussionContext",
|
|
27
|
+
"getProductDocument",
|
|
28
|
+
"putProductDocument",
|
|
29
|
+
"getCampaignAuthorization",
|
|
30
|
+
"submitProductTaskIntent",
|
|
31
|
+
"recordProductGapReview",
|
|
32
|
+
"recordProductGoalSatisfied",
|
|
33
|
+
]);
|
|
20
34
|
const systemObserverReasonResultSchema = z
|
|
21
35
|
.object({
|
|
22
36
|
scope: z.string().min(1).max(240).optional(),
|
|
@@ -207,7 +221,7 @@ async function responseJson(response) {
|
|
|
207
221
|
});
|
|
208
222
|
return value;
|
|
209
223
|
}
|
|
210
|
-
function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
|
|
224
|
+
function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection, shutdownSignal) {
|
|
211
225
|
const connection = async () => {
|
|
212
226
|
const resolved = resolveConnection
|
|
213
227
|
? await resolveConnection(owner)
|
|
@@ -319,7 +333,9 @@ function createOwnerHttpClient(owner, baseUrl, credential, resolveConnection) {
|
|
|
319
333
|
...(method === "POST"
|
|
320
334
|
? { body: JSON.stringify(requestBody) }
|
|
321
335
|
: {}),
|
|
322
|
-
signal:
|
|
336
|
+
signal: shutdownSignal
|
|
337
|
+
? AbortSignal.any([shutdownSignal, AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS)])
|
|
338
|
+
: AbortSignal.timeout(OWNER_INVOKE_WATCHDOG_MS),
|
|
323
339
|
}));
|
|
324
340
|
},
|
|
325
341
|
});
|
|
@@ -382,7 +398,7 @@ async function readDeploymentOwnerSummary(stateRoot) {
|
|
|
382
398
|
return undefined;
|
|
383
399
|
}
|
|
384
400
|
}
|
|
385
|
-
async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection) {
|
|
401
|
+
async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection, resolveMonitorControlConnection) {
|
|
386
402
|
const sink = createOperationSink(config.stateRoot);
|
|
387
403
|
const observer = createHostOperationObserver(entry => sink.write("platform-host", entry));
|
|
388
404
|
const databasePath = join(config.stateRoot, "state", "task.sqlite");
|
|
@@ -445,9 +461,37 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
445
461
|
taskStore.close();
|
|
446
462
|
throw error;
|
|
447
463
|
}
|
|
448
|
-
const
|
|
449
|
-
const
|
|
464
|
+
const ownerShutdown = new AbortController();
|
|
465
|
+
const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection, ownerShutdown.signal));
|
|
466
|
+
const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection, ownerShutdown.signal));
|
|
467
|
+
const monitor = resolveMonitorControlConnection
|
|
468
|
+
? Object.freeze({
|
|
469
|
+
async invoke(operation, input = {}) {
|
|
470
|
+
const connection = await resolveMonitorControlConnection();
|
|
471
|
+
if (!connection)
|
|
472
|
+
throw new Error("MONITOR_CONTROL_UNAVAILABLE");
|
|
473
|
+
return createMonitorControlClient({
|
|
474
|
+
endpoint: connection.endpoint,
|
|
475
|
+
token: connection.credential,
|
|
476
|
+
}).invoke(operation, input);
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
: undefined;
|
|
450
480
|
let reconciliationCoordinator;
|
|
481
|
+
let productCampaignContinuationCoordinator;
|
|
482
|
+
const monitorRotationCoordinator = monitor
|
|
483
|
+
? createMonitorRotationCoordinator({
|
|
484
|
+
monitor,
|
|
485
|
+
execution,
|
|
486
|
+
onError: (errorCode) => sink.write("platform-host", {
|
|
487
|
+
timestamp: new Date().toISOString(),
|
|
488
|
+
level: "WARN",
|
|
489
|
+
component: "platform-host-monitor-rotation",
|
|
490
|
+
event: "MONITOR_ROTATION_POLL_FAILED",
|
|
491
|
+
errorCode,
|
|
492
|
+
}),
|
|
493
|
+
})
|
|
494
|
+
: undefined;
|
|
451
495
|
const boundedSystemView = async (view) => {
|
|
452
496
|
if (view === "task") {
|
|
453
497
|
const tasks = unwrap(task.queries.listTasks({})).tasks;
|
|
@@ -649,8 +693,59 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
649
693
|
throw Object.assign(new Error(code), { httpStatus });
|
|
650
694
|
}
|
|
651
695
|
}
|
|
652
|
-
const taskOperation =
|
|
696
|
+
const taskOperation = operationId === "getProductDiscussionContext"
|
|
697
|
+
? () => {
|
|
698
|
+
const session = agent.getProductDiscussionSession(string(input.discussionRef, "discussionRef"));
|
|
699
|
+
return { ok: true, data: { ...session, discussionGeneration: session.generation } };
|
|
700
|
+
}
|
|
701
|
+
: taskOperations.get(operationId);
|
|
653
702
|
if (taskOperation) {
|
|
703
|
+
const productOperation = role.agentPackageRef === "@tomflow/proflow-agent-product" &&
|
|
704
|
+
productCampaignOperationIds.has(operationId)
|
|
705
|
+
? canonicalizeProductOperation({
|
|
706
|
+
operationId,
|
|
707
|
+
authenticatedRoleRef,
|
|
708
|
+
rawInput: input,
|
|
709
|
+
getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
|
|
710
|
+
getRoleBindings: () => rolePackageRefs.map((agentPackageRef) => ({
|
|
711
|
+
agentPackageRef,
|
|
712
|
+
roleRef: roleForPackage(agentPackageRef).roleRef,
|
|
713
|
+
})),
|
|
714
|
+
verifyDiscussionAdmission: (admission) => {
|
|
715
|
+
if (!productDiscussionHost?.verifyAdmission(admission))
|
|
716
|
+
return false;
|
|
717
|
+
return !unwrap(task.queries.listTasks({})).tasks.some((summary) => unwrap(task.queries.getTask({ taskId: summary.taskId })).roleBindings.some((binding) => binding.conversationLocator === admission.conversationLocator ||
|
|
718
|
+
binding.workerRef === admission.conversationRef));
|
|
719
|
+
},
|
|
720
|
+
})
|
|
721
|
+
: undefined;
|
|
722
|
+
if (productOperation?.scope === "PRODUCT_QUERY") {
|
|
723
|
+
if (operationId === "getCampaignAuthorization") {
|
|
724
|
+
const authorization = unwrap(task.queries.getCampaignAuthorization(productOperation.input));
|
|
725
|
+
if (authorization.campaignRef !== input.campaignRef ||
|
|
726
|
+
authorization.discussionRef !== input.discussionRef)
|
|
727
|
+
throw Object.assign(new Error("PRODUCT_DISCUSSION_CAMPAIGN_MISMATCH"), { httpStatus: 403 });
|
|
728
|
+
}
|
|
729
|
+
return taskOperation(productOperation.input);
|
|
730
|
+
}
|
|
731
|
+
if (productOperation?.scope === "PRODUCT_MUTATION") {
|
|
732
|
+
const taskResult = await taskOperation({
|
|
733
|
+
...productOperation.input,
|
|
734
|
+
actorRef: productOperation.actorRef,
|
|
735
|
+
});
|
|
736
|
+
if (productOperation.kickContinuation &&
|
|
737
|
+
typeof taskResult === "object" &&
|
|
738
|
+
taskResult !== null &&
|
|
739
|
+
Reflect.get(taskResult, "ok") === true) {
|
|
740
|
+
const data = Reflect.get(taskResult, "data");
|
|
741
|
+
if (typeof data === "object" && data !== null) {
|
|
742
|
+
const taskId = Reflect.get(data, "taskId");
|
|
743
|
+
if (typeof taskId === "string")
|
|
744
|
+
productCampaignContinuationCoordinator?.kick(taskId);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return taskResult;
|
|
748
|
+
}
|
|
654
749
|
let actorRef = authenticatedRoleRef;
|
|
655
750
|
let canonicalWorkerRef;
|
|
656
751
|
if (typeof input.taskId === "string") {
|
|
@@ -691,8 +786,10 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
691
786
|
const taskResult = await taskOperation(queryOperations.has(operationId)
|
|
692
787
|
? canonicalTaskInput
|
|
693
788
|
: { ...canonicalTaskInput, actorRef });
|
|
694
|
-
if (taskMutationOperations.has(operationId) && typeof input.taskId === "string")
|
|
789
|
+
if (taskMutationOperations.has(operationId) && typeof input.taskId === "string") {
|
|
695
790
|
reconciliationCoordinator?.kick(input.taskId);
|
|
791
|
+
productCampaignContinuationCoordinator?.kick(input.taskId);
|
|
792
|
+
}
|
|
696
793
|
if (operationId === "getTaskDocument")
|
|
697
794
|
return fileBridgeOutputForTaskResult(taskResult);
|
|
698
795
|
return taskResult;
|
|
@@ -713,8 +810,14 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
713
810
|
const declared = current.roleBindings.find((item) => item.roleRef === binding.roleRef);
|
|
714
811
|
if (!declared)
|
|
715
812
|
throw new Error("AGENT_PACKAGE_NOT_ELIGIBLE");
|
|
716
|
-
if (declared.workerRef === binding.workerRef)
|
|
813
|
+
if (declared.workerRef === binding.workerRef) {
|
|
814
|
+
if (declared.conversationLocator !== null &&
|
|
815
|
+
declared.conversationLocator !== binding.conversationLocator)
|
|
816
|
+
throw new Error("TASK_ROLE_BINDING_CONVERSATION_MISMATCH");
|
|
817
|
+
if (declared.conversationLocator === binding.conversationLocator)
|
|
818
|
+
productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
|
|
717
819
|
return;
|
|
820
|
+
}
|
|
718
821
|
if (declared.workerRef)
|
|
719
822
|
throw new Error("TASK_ROLE_BINDING_CONFLICT");
|
|
720
823
|
unwrap(task.commands.bindTaskWorker({
|
|
@@ -727,7 +830,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
727
830
|
actorRef: "platform-host:worker-provisioning",
|
|
728
831
|
idempotencyKey: `browser-bind:${binding.taskId}:${binding.roleRef}:${binding.workerRef}`,
|
|
729
832
|
}));
|
|
833
|
+
productDiscussionHost?.releaseTaskWorker(binding.taskId, binding.roleRef);
|
|
730
834
|
reconciliationCoordinator?.kick(binding.taskId);
|
|
835
|
+
productCampaignContinuationCoordinator?.kick(binding.taskId);
|
|
731
836
|
},
|
|
732
837
|
}),
|
|
733
838
|
agent: Object.freeze({
|
|
@@ -764,6 +869,62 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
764
869
|
});
|
|
765
870
|
const authorizeExecution = async (request) => {
|
|
766
871
|
try {
|
|
872
|
+
if (request.capability === "product.discussion.bootstrap.deliver")
|
|
873
|
+
return productDiscussionHost
|
|
874
|
+
? productDiscussionHost.authorizeBootstrapExecution(request)
|
|
875
|
+
: false;
|
|
876
|
+
if (request.capability === "monitor.chat.submit" ||
|
|
877
|
+
request.capability === "monitor.chat.create") {
|
|
878
|
+
if (!monitor)
|
|
879
|
+
return false;
|
|
880
|
+
return authorizeMonitorExecution({ monitor, request: request });
|
|
881
|
+
}
|
|
882
|
+
if (request.capability === "product.review.deliver") {
|
|
883
|
+
if (request.callerRef !== "platform-host:campaign-continuation")
|
|
884
|
+
return false;
|
|
885
|
+
if (request.taskId !== undefined ||
|
|
886
|
+
request.nodeId !== undefined ||
|
|
887
|
+
request.runNo !== undefined ||
|
|
888
|
+
request.workerRef !== undefined ||
|
|
889
|
+
request.projectRoot !== undefined)
|
|
890
|
+
return false;
|
|
891
|
+
const reviewInput = object(request.input, "product review execution input");
|
|
892
|
+
const reviewRef = string(reviewInput.reviewRef, "input.reviewRef");
|
|
893
|
+
const discussionRef = string(reviewInput.discussionRef, "input.discussionRef");
|
|
894
|
+
const discussionGeneration = positiveInteger(reviewInput.discussionGeneration, "input.discussionGeneration");
|
|
895
|
+
const roleRef = string(reviewInput.roleRef, "input.roleRef");
|
|
896
|
+
const conversationRef = string(reviewInput.conversationRef, "input.conversationRef");
|
|
897
|
+
const conversationLocator = string(reviewInput.conversationLocator, "input.conversationLocator");
|
|
898
|
+
const campaignRef = string(reviewInput.campaignRef, "input.campaignRef");
|
|
899
|
+
const intentRef = string(reviewInput.intentRef, "input.intentRef");
|
|
900
|
+
const terminalTaskId = string(reviewInput.terminalTaskId, "input.terminalTaskId");
|
|
901
|
+
const terminalTaskVersion = positiveInteger(reviewInput.terminalTaskVersion, "input.terminalTaskVersion");
|
|
902
|
+
if (request.roleRef !== roleRef)
|
|
903
|
+
return false;
|
|
904
|
+
const productRole = agent
|
|
905
|
+
.listRegisteredRoles()
|
|
906
|
+
.find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
|
|
907
|
+
if (!productRole || productRole.roleRef !== roleRef)
|
|
908
|
+
return false;
|
|
909
|
+
const session = agent.getProductDiscussionSession(discussionRef);
|
|
910
|
+
if (session.status !== "ACTIVE" ||
|
|
911
|
+
session.roleRef !== roleRef ||
|
|
912
|
+
session.generation !== discussionGeneration ||
|
|
913
|
+
session.conversationRef !== conversationRef ||
|
|
914
|
+
session.conversationLocator !== conversationLocator ||
|
|
915
|
+
session.campaignRef !== campaignRef ||
|
|
916
|
+
session.currentIntentRef !== intentRef)
|
|
917
|
+
return false;
|
|
918
|
+
const terminalTask = unwrap(task.queries.getTask({ taskId: terminalTaskId }));
|
|
919
|
+
if (!taskIsTerminal(terminalTask.status) ||
|
|
920
|
+
terminalTask.version !== terminalTaskVersion)
|
|
921
|
+
return false;
|
|
922
|
+
const expectedReviewRef = `product-review:${campaignRef}:${terminalTaskId}:${terminalTaskVersion}:${discussionRef}:${discussionGeneration}`;
|
|
923
|
+
if (reviewRef !== expectedReviewRef ||
|
|
924
|
+
reviewInput.contentFingerprint !== expectedReviewRef)
|
|
925
|
+
return false;
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
767
928
|
const browserCapability = request.capability === "worker.create" ||
|
|
768
929
|
request.capability === "worker.restore" ||
|
|
769
930
|
request.capability === "worker.wake" ||
|
|
@@ -908,6 +1069,29 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
908
1069
|
throw new Error("ROLE_NOT_FOUND");
|
|
909
1070
|
return role;
|
|
910
1071
|
};
|
|
1072
|
+
const productRole = agent
|
|
1073
|
+
.listRegisteredRoles()
|
|
1074
|
+
.find((candidate) => candidate.agentPackageRef === "@tomflow/proflow-agent-product");
|
|
1075
|
+
const productDiscussionHost = productRole
|
|
1076
|
+
? createProductDiscussionHost({
|
|
1077
|
+
productRoleRef: productRole.roleRef,
|
|
1078
|
+
listTaskIds: () => unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId),
|
|
1079
|
+
getTaskFact: (taskId) => {
|
|
1080
|
+
const current = unwrap(task.queries.getTask({ taskId }));
|
|
1081
|
+
return {
|
|
1082
|
+
taskId: current.taskId,
|
|
1083
|
+
status: current.status,
|
|
1084
|
+
roleBindings: current.roleBindings,
|
|
1085
|
+
};
|
|
1086
|
+
},
|
|
1087
|
+
registerDiscussionSession: (input) => agent.registerProductDiscussionSession(input),
|
|
1088
|
+
getDiscussionSession: (discussionRef) => agent.getProductDiscussionSession(discussionRef),
|
|
1089
|
+
async executeCapability(request) {
|
|
1090
|
+
return execution.invoke("executeCapability", request);
|
|
1091
|
+
},
|
|
1092
|
+
})
|
|
1093
|
+
: undefined;
|
|
1094
|
+
await productDiscussionHost?.recoverReservations();
|
|
911
1095
|
const browserPermissionRole = (roleRef) => {
|
|
912
1096
|
const role = agent.listRegisteredRoles().find((candidate) => candidate.roleRef === roleRef);
|
|
913
1097
|
if (!role || !rolePackageRefs.includes(role.agentPackageRef))
|
|
@@ -953,6 +1137,49 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
953
1137
|
}
|
|
954
1138
|
if (operation === "role.list")
|
|
955
1139
|
return agent.listRegisteredRoles();
|
|
1140
|
+
if (operation === "product.campaign.authorize") {
|
|
1141
|
+
const discussionRef = string(value.discussionRef, "discussionRef");
|
|
1142
|
+
const campaignRef = string(value.campaignRef, "campaignRef");
|
|
1143
|
+
const goalRevision = positiveInteger(value.goalRevision, "goalRevision");
|
|
1144
|
+
const productRole = roleForPackage("@tomflow/proflow-agent-product");
|
|
1145
|
+
const before = agent.getProductDiscussionSession(discussionRef);
|
|
1146
|
+
if (before.status !== "ACTIVE" || before.roleRef !== productRole.roleRef)
|
|
1147
|
+
throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
|
|
1148
|
+
const granted = unwrap(task.commands.createCampaignAuthorization({
|
|
1149
|
+
...(value.authorizationRef === undefined
|
|
1150
|
+
? {}
|
|
1151
|
+
: { authorizationRef: string(value.authorizationRef, "authorizationRef") }),
|
|
1152
|
+
campaignRef,
|
|
1153
|
+
discussionRef,
|
|
1154
|
+
goalRevision,
|
|
1155
|
+
goalSummary: string(value.goalSummary, "goalSummary"),
|
|
1156
|
+
scope: value.scope,
|
|
1157
|
+
constraints: value.constraints,
|
|
1158
|
+
actorRef: "trusted:product-control",
|
|
1159
|
+
idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
|
|
1160
|
+
}));
|
|
1161
|
+
const current = agent.getProductDiscussionSession(discussionRef);
|
|
1162
|
+
if (current.status !== "ACTIVE" || current.roleRef !== productRole.roleRef)
|
|
1163
|
+
throw new Error("PRODUCT_DISCUSSION_SESSION_NOT_READY");
|
|
1164
|
+
const discussionSession = current.campaignRef === campaignRef &&
|
|
1165
|
+
current.goalRevision === goalRevision
|
|
1166
|
+
? current
|
|
1167
|
+
: agent.updateProductDiscussionSessionContext({
|
|
1168
|
+
discussionRef,
|
|
1169
|
+
expectedVersion: current.version,
|
|
1170
|
+
campaignRef,
|
|
1171
|
+
goalRevision,
|
|
1172
|
+
});
|
|
1173
|
+
return { ...granted, discussionSession };
|
|
1174
|
+
}
|
|
1175
|
+
if (operation === "product.campaign.revoke")
|
|
1176
|
+
return unwrap(task.commands.revokeCampaignAuthorization({
|
|
1177
|
+
authorizationRef: string(value.authorizationRef, "authorizationRef"),
|
|
1178
|
+
expectedAuthorizationVersion: positiveInteger(value.expectedAuthorizationVersion, "expectedAuthorizationVersion"),
|
|
1179
|
+
reason: string(value.reason, "reason"),
|
|
1180
|
+
actorRef: "trusted:product-control",
|
|
1181
|
+
idempotencyKey: string(value.idempotencyKey, "idempotencyKey"),
|
|
1182
|
+
}));
|
|
956
1183
|
const agentPackageRef = string(value.agentPackageRef, "agentPackageRef");
|
|
957
1184
|
const role = roleForPackage(agentPackageRef);
|
|
958
1185
|
if (operation === "role.show")
|
|
@@ -989,9 +1216,12 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
989
1216
|
const binding = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
|
|
990
1217
|
if (!binding)
|
|
991
1218
|
throw new Error("TASK_ROLE_BINDING_REQUIRED");
|
|
992
|
-
if (binding.workerRef && binding.conversationLocator)
|
|
1219
|
+
if (binding.workerRef && binding.conversationLocator) {
|
|
1220
|
+
productDiscussionHost?.releaseTaskWorker(taskId, binding.roleRef);
|
|
993
1221
|
return;
|
|
1222
|
+
}
|
|
994
1223
|
const role = roleForPackage(agentPackageRef);
|
|
1224
|
+
productDiscussionHost?.reserveTaskWorker(taskId, role.roleRef);
|
|
995
1225
|
const executionRecord = object(await execution.invoke("executeCapability", {
|
|
996
1226
|
contract: "execution",
|
|
997
1227
|
contractVersion: "1.0.0",
|
|
@@ -1007,12 +1237,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1007
1237
|
bootstrapFingerprint: `new-task:${taskId}:${agentPackageRef}`,
|
|
1008
1238
|
},
|
|
1009
1239
|
}), "worker create execution");
|
|
1010
|
-
if (executionRecord.status !== "SUCCEEDED")
|
|
1240
|
+
if (executionRecord.status !== "SUCCEEDED") {
|
|
1241
|
+
current = unwrap(task.queries.getTask({ taskId }));
|
|
1242
|
+
const observed = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
|
|
1243
|
+
if (observed?.workerRef && observed.conversationLocator)
|
|
1244
|
+
productDiscussionHost?.releaseTaskWorker(taskId, observed.roleRef);
|
|
1011
1245
|
throw new Error(`WORKER_CREATE_NOT_CONFIRMED:${String(executionRecord.status)}`);
|
|
1246
|
+
}
|
|
1012
1247
|
current = unwrap(task.queries.getTask({ taskId }));
|
|
1013
1248
|
const persisted = current.roleBindings.find((candidate) => candidate.agentPackageRef === agentPackageRef);
|
|
1014
1249
|
if (!persisted?.workerRef || !persisted.conversationLocator)
|
|
1015
1250
|
throw new Error("WORKER_CREATE_BINDING_NOT_PERSISTED");
|
|
1251
|
+
productDiscussionHost?.releaseTaskWorker(taskId, persisted.roleRef);
|
|
1016
1252
|
};
|
|
1017
1253
|
const waitFor = new Set(options?.waitFor ?? []);
|
|
1018
1254
|
const shouldWait = (agentPackageRef) => waitFor.size === 0 || waitFor.has(roleForPackage(agentPackageRef).roleRef);
|
|
@@ -1081,6 +1317,166 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1081
1317
|
getProjection: (taskId) => taskDriverPorts.getTaskDriveProjection(taskId),
|
|
1082
1318
|
requestWake: requestTaskWake,
|
|
1083
1319
|
});
|
|
1320
|
+
productCampaignContinuationCoordinator = createProductCampaignContinuationCoordinator({
|
|
1321
|
+
async getProjection(taskId) {
|
|
1322
|
+
const result = task.queries.getProductContinuationProjection({ taskId });
|
|
1323
|
+
if (!result.ok) {
|
|
1324
|
+
const code = typeof result.error === "object" && result.error !== null
|
|
1325
|
+
? Reflect.get(result.error, "code")
|
|
1326
|
+
: undefined;
|
|
1327
|
+
if (code === "PRODUCT_CAMPAIGN_NOT_FOUND")
|
|
1328
|
+
return null;
|
|
1329
|
+
throw new Error(`PRODUCT_CONTINUATION_PROJECTION_FAILED:${String(code ?? "UNKNOWN")}`);
|
|
1330
|
+
}
|
|
1331
|
+
return result.data;
|
|
1332
|
+
},
|
|
1333
|
+
async listTaskIds() {
|
|
1334
|
+
return unwrap(task.queries.listTasks({})).tasks.map((summary) => summary.taskId);
|
|
1335
|
+
},
|
|
1336
|
+
async ensureWorkers(taskId) {
|
|
1337
|
+
await ensureTaskWorkers(taskId);
|
|
1338
|
+
},
|
|
1339
|
+
async transferRequirement(input) {
|
|
1340
|
+
unwrap(task.commands.transferProductIntentToTask({
|
|
1341
|
+
...input,
|
|
1342
|
+
actorRef: "platform:campaign-continuation",
|
|
1343
|
+
}));
|
|
1344
|
+
},
|
|
1345
|
+
async startTask(input) {
|
|
1346
|
+
unwrap(task.commands.startTask({
|
|
1347
|
+
taskId: input.taskId,
|
|
1348
|
+
expectedTaskVersion: input.expectedTaskVersion,
|
|
1349
|
+
startAuthority: {
|
|
1350
|
+
type: "CAMPAIGN_AUTHORIZATION",
|
|
1351
|
+
authorizationRef: input.authorizationRef,
|
|
1352
|
+
expectedAuthorizationVersion: input.expectedAuthorizationVersion,
|
|
1353
|
+
expectedCampaignVersion: input.expectedCampaignVersion,
|
|
1354
|
+
},
|
|
1355
|
+
actorRef: "platform:campaign-continuation",
|
|
1356
|
+
idempotencyKey: input.idempotencyKey,
|
|
1357
|
+
}));
|
|
1358
|
+
},
|
|
1359
|
+
getDiscussionSessionForCampaign: (campaignRef) => agent.getProductDiscussionSessionForCampaign(campaignRef),
|
|
1360
|
+
updateDiscussionSessionContext: (input) => agent.updateProductDiscussionSessionContext(input),
|
|
1361
|
+
kickTaskReconciliation: (taskId) => reconciliationCoordinator?.kick(taskId),
|
|
1362
|
+
async onProductReviewRequired(signal) {
|
|
1363
|
+
const session = agent.getProductDiscussionSession(signal.discussionRef);
|
|
1364
|
+
if (session.status !== "ACTIVE" ||
|
|
1365
|
+
session.roleRef !== signal.roleRef ||
|
|
1366
|
+
session.generation !== signal.discussionGeneration ||
|
|
1367
|
+
session.conversationRef !== signal.conversationRef ||
|
|
1368
|
+
session.conversationLocator !== signal.conversationLocator ||
|
|
1369
|
+
session.campaignRef !== signal.campaignRef ||
|
|
1370
|
+
session.currentIntentRef !== signal.intentRef)
|
|
1371
|
+
throw new Error("PRODUCT_REVIEW_DISCUSSION_STALE");
|
|
1372
|
+
const reviewRequest = {
|
|
1373
|
+
contract: "execution",
|
|
1374
|
+
contractVersion: "1.0.0",
|
|
1375
|
+
idempotencyKey: signal.reviewRef,
|
|
1376
|
+
callerRef: "platform-host:campaign-continuation",
|
|
1377
|
+
correlationId: signal.reviewRef,
|
|
1378
|
+
roleRef: signal.roleRef,
|
|
1379
|
+
capability: "product.review.deliver",
|
|
1380
|
+
input: {
|
|
1381
|
+
reviewRef: signal.reviewRef,
|
|
1382
|
+
discussionRef: signal.discussionRef,
|
|
1383
|
+
discussionGeneration: signal.discussionGeneration,
|
|
1384
|
+
roleRef: signal.roleRef,
|
|
1385
|
+
conversationRef: signal.conversationRef,
|
|
1386
|
+
conversationLocator: signal.conversationLocator,
|
|
1387
|
+
campaignRef: signal.campaignRef,
|
|
1388
|
+
intentRef: signal.intentRef,
|
|
1389
|
+
terminalTaskId: signal.taskId,
|
|
1390
|
+
terminalTaskVersion: signal.terminalTaskVersion,
|
|
1391
|
+
contentFingerprint: signal.reviewRef,
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
if (!(await authorizeExecution(reviewRequest)))
|
|
1395
|
+
throw new Error("PRODUCT_REVIEW_EXECUTION_IDENTITY_DENIED");
|
|
1396
|
+
let executionRecord;
|
|
1397
|
+
try {
|
|
1398
|
+
executionRecord = object(await execution.invoke("executeCapability", reviewRequest), "product review delivery execution");
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
await sink.write("platform-host", {
|
|
1402
|
+
timestamp: new Date().toISOString(),
|
|
1403
|
+
level: "WARN",
|
|
1404
|
+
component: "platform-host-product-campaign-continuation",
|
|
1405
|
+
event: "PRODUCT_REVIEW_REQUIRED",
|
|
1406
|
+
eventId: signal.reviewRef,
|
|
1407
|
+
correlationId: signal.reviewRef,
|
|
1408
|
+
status: "UNKNOWN",
|
|
1409
|
+
taskId: signal.taskId,
|
|
1410
|
+
roleRef: signal.roleRef,
|
|
1411
|
+
conversationLocator: signal.conversationLocator,
|
|
1412
|
+
campaignRef: signal.campaignRef,
|
|
1413
|
+
intentRef: signal.intentRef,
|
|
1414
|
+
discussionRef: signal.discussionRef,
|
|
1415
|
+
discussionGeneration: signal.discussionGeneration,
|
|
1416
|
+
terminalTaskVersion: signal.terminalTaskVersion,
|
|
1417
|
+
errorCode: "EXECUTION_OWNER_RESPONSE_UNKNOWN",
|
|
1418
|
+
});
|
|
1419
|
+
throw error;
|
|
1420
|
+
}
|
|
1421
|
+
const status = string(executionRecord.status, "execution.status");
|
|
1422
|
+
const sideEffectState = string(executionRecord.sideEffectState, "execution.sideEffectState");
|
|
1423
|
+
const executionRef = typeof executionRecord.executionRef === "string"
|
|
1424
|
+
? executionRecord.executionRef
|
|
1425
|
+
: undefined;
|
|
1426
|
+
const executionResult = typeof executionRecord.result === "object" &&
|
|
1427
|
+
executionRecord.result !== null &&
|
|
1428
|
+
!Array.isArray(executionRecord.result)
|
|
1429
|
+
? executionRecord.result
|
|
1430
|
+
: undefined;
|
|
1431
|
+
const resultData = executionResult &&
|
|
1432
|
+
typeof executionResult.data === "object" &&
|
|
1433
|
+
executionResult.data !== null &&
|
|
1434
|
+
!Array.isArray(executionResult.data)
|
|
1435
|
+
? executionResult.data
|
|
1436
|
+
: undefined;
|
|
1437
|
+
const evidenceRef = executionResult?.capability === "product.review.deliver" &&
|
|
1438
|
+
resultData?.reviewRef === signal.reviewRef &&
|
|
1439
|
+
resultData.delivered === true &&
|
|
1440
|
+
typeof resultData.evidenceRef === "string"
|
|
1441
|
+
? resultData.evidenceRef
|
|
1442
|
+
: undefined;
|
|
1443
|
+
const delivered = status === "SUCCEEDED" &&
|
|
1444
|
+
sideEffectState === "APPLIED" &&
|
|
1445
|
+
evidenceRef !== undefined;
|
|
1446
|
+
const error = typeof executionRecord.error === "object" &&
|
|
1447
|
+
executionRecord.error !== null &&
|
|
1448
|
+
!Array.isArray(executionRecord.error)
|
|
1449
|
+
? executionRecord.error
|
|
1450
|
+
: undefined;
|
|
1451
|
+
const errorCode = typeof error?.code === "string" ? error.code : undefined;
|
|
1452
|
+
await sink.write("platform-host", {
|
|
1453
|
+
timestamp: new Date().toISOString(),
|
|
1454
|
+
level: delivered ? "INFO" : "WARN",
|
|
1455
|
+
component: "platform-host-product-campaign-continuation",
|
|
1456
|
+
event: "PRODUCT_REVIEW_REQUIRED",
|
|
1457
|
+
eventId: signal.reviewRef,
|
|
1458
|
+
correlationId: signal.reviewRef,
|
|
1459
|
+
status: delivered
|
|
1460
|
+
? "DELIVERED"
|
|
1461
|
+
: status === "UNKNOWN"
|
|
1462
|
+
? "UNKNOWN"
|
|
1463
|
+
: "FAILED",
|
|
1464
|
+
taskId: signal.taskId,
|
|
1465
|
+
roleRef: signal.roleRef,
|
|
1466
|
+
conversationLocator: signal.conversationLocator,
|
|
1467
|
+
campaignRef: signal.campaignRef,
|
|
1468
|
+
intentRef: signal.intentRef,
|
|
1469
|
+
discussionRef: signal.discussionRef,
|
|
1470
|
+
discussionGeneration: signal.discussionGeneration,
|
|
1471
|
+
terminalTaskVersion: signal.terminalTaskVersion,
|
|
1472
|
+
...(executionRef ? { executionRef } : {}),
|
|
1473
|
+
...(evidenceRef ? { evidenceRef } : {}),
|
|
1474
|
+
...(errorCode ? { errorCode } : {}),
|
|
1475
|
+
});
|
|
1476
|
+
if (!delivered)
|
|
1477
|
+
throw new Error(`PRODUCT_REVIEW_DELIVERY_NOT_CONFIRMED:${status}:${sideEffectState}`);
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1084
1480
|
const taskApplication = observer.port("task", {
|
|
1085
1481
|
async invoke(operation, rawInput) {
|
|
1086
1482
|
const value = object(rawInput, "task application input");
|
|
@@ -1243,6 +1639,18 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1243
1639
|
void reconciliationCoordinator?.sweep();
|
|
1244
1640
|
return { scheduled: true };
|
|
1245
1641
|
}
|
|
1642
|
+
if (operation === "product.discussion.register")
|
|
1643
|
+
throw new Error("PRODUCT_DISCUSSION_REGISTER_DIRECT_FORBIDDEN");
|
|
1644
|
+
if (operation === "product.discussion.observe") {
|
|
1645
|
+
if (!productDiscussionHost)
|
|
1646
|
+
throw new Error("PRODUCT_DISCUSSION_ADMISSION_UNAVAILABLE");
|
|
1647
|
+
return productDiscussionHost.observe({
|
|
1648
|
+
conversationLocator: string(value.conversationLocator, "conversationLocator"),
|
|
1649
|
+
contentInstanceId: string(value.contentInstanceId, "contentInstanceId"),
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
if (operation === "product.discussion.get")
|
|
1653
|
+
return agent.getProductDiscussionSession(string(value.discussionRef, "discussionRef"));
|
|
1246
1654
|
if (operation === "browser.permission.classify") {
|
|
1247
1655
|
const roleRef = string(value.roleRef, "roleRef");
|
|
1248
1656
|
if (value.taskId !== undefined && typeof value.taskId !== "string")
|
|
@@ -1374,6 +1782,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1374
1782
|
},
|
|
1375
1783
|
});
|
|
1376
1784
|
reconciliationCoordinator.start();
|
|
1785
|
+
productCampaignContinuationCoordinator.start();
|
|
1377
1786
|
return Object.freeze({
|
|
1378
1787
|
route,
|
|
1379
1788
|
operationSink: sink,
|
|
@@ -1385,6 +1794,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1385
1794
|
taskApplication,
|
|
1386
1795
|
approvalApplication,
|
|
1387
1796
|
observerApplication,
|
|
1797
|
+
startMonitorRotation() {
|
|
1798
|
+
monitorRotationCoordinator?.start();
|
|
1799
|
+
},
|
|
1388
1800
|
async lookup(_operationId, _authenticatedRoleRef, _input) {
|
|
1389
1801
|
throw Object.assign(new Error("ACTION_RESULT_LOOKUP_UNSUPPORTED"), { httpStatus: 409 });
|
|
1390
1802
|
},
|
|
@@ -1414,8 +1826,17 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1414
1826
|
model: modelStatus,
|
|
1415
1827
|
};
|
|
1416
1828
|
},
|
|
1417
|
-
close() {
|
|
1829
|
+
async close() {
|
|
1830
|
+
productCampaignContinuationCoordinator?.stop();
|
|
1418
1831
|
reconciliationCoordinator?.stop();
|
|
1832
|
+
await monitorRotationCoordinator?.stop();
|
|
1833
|
+
ownerShutdown.abort();
|
|
1834
|
+
await Promise.all([
|
|
1835
|
+
productCampaignContinuationCoordinator?.drain(),
|
|
1836
|
+
reconciliationCoordinator?.drain(),
|
|
1837
|
+
]);
|
|
1838
|
+
productDiscussionHost?.clear();
|
|
1839
|
+
await sink.drain();
|
|
1419
1840
|
agent.close();
|
|
1420
1841
|
taskStore.close();
|
|
1421
1842
|
},
|
|
@@ -1659,9 +2080,9 @@ export function createPlatformHost(input) {
|
|
|
1659
2080
|
? await readExecutionTransportCredential(input.config.executionTransportCredentialFile)
|
|
1660
2081
|
: input.executionCredential;
|
|
1661
2082
|
log("DEPENDENCY_INITIALIZATION_STARTED", {
|
|
1662
|
-
order: ["task", "agent", "execution-client", "model-client"],
|
|
2083
|
+
order: ["task", "agent", "execution-client", "model-client", "monitor-control-client"],
|
|
1663
2084
|
});
|
|
1664
|
-
graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
|
|
2085
|
+
graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection, input.resolveMonitorControlConnection);
|
|
1665
2086
|
server = createServer((request, response) => {
|
|
1666
2087
|
const work = operationContext.run({ operationRef: incomingOperationRef(request.headers["x-proflow-operation-ref"]) }, async () => {
|
|
1667
2088
|
const url = new URL(request.url ?? "/", "http://platform-host.local");
|
|
@@ -1875,6 +2296,7 @@ export function createPlatformHost(input) {
|
|
|
1875
2296
|
const address = server.address();
|
|
1876
2297
|
if (!address || typeof address === "string")
|
|
1877
2298
|
throw new Error("platform-host missing TCP address");
|
|
2299
|
+
graph.startMonitorRotation();
|
|
1878
2300
|
log("SERVICE_STARTED", { host: input.config.host, port: address.port });
|
|
1879
2301
|
return { host: input.config.host, port: address.port };
|
|
1880
2302
|
}
|
|
@@ -1882,7 +2304,7 @@ export function createPlatformHost(input) {
|
|
|
1882
2304
|
accepting = false;
|
|
1883
2305
|
server?.close();
|
|
1884
2306
|
server = undefined;
|
|
1885
|
-
graph?.close();
|
|
2307
|
+
await graph?.close();
|
|
1886
2308
|
graph = undefined;
|
|
1887
2309
|
roleManagementCredential = undefined;
|
|
1888
2310
|
taskApplicationCredential = undefined;
|
|
@@ -1902,7 +2324,7 @@ export function createPlatformHost(input) {
|
|
|
1902
2324
|
if (running)
|
|
1903
2325
|
await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
|
|
1904
2326
|
await Promise.allSettled([...active]);
|
|
1905
|
-
graph?.close();
|
|
2327
|
+
await graph?.close();
|
|
1906
2328
|
graph = undefined;
|
|
1907
2329
|
roleManagementCredential = undefined;
|
|
1908
2330
|
taskApplicationCredential = undefined;
|