@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
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/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { createPermissionReviewer } from './agent-permission-review.js';
|
|
2
|
+
import { createAgentStackController } from './agent-stack-controller.js';
|
|
3
|
+
import { createAgentPersonaController } from './agent-persona-controller.js';
|
|
4
|
+
import { composeAgentPrompt } from './agent-prompt-composition.js';
|
|
5
|
+
import { legacyAgentStackIdentityWarning } from './agent-identity-migration.js';
|
|
6
|
+
import { assembleContextBundle } from './agent-context-bundles.js';
|
|
7
|
+
import { createResource, clone } from './resource-model.js';
|
|
8
|
+
import { createAgentMuxClient } from './agent-mux-client.js';
|
|
9
|
+
import { createAgentMemoryController } from './agent-memory-controller.js';
|
|
10
|
+
import { createAgentApprovalController } from './agent-approval-controller.js';
|
|
11
|
+
import { createAgentWorkspaceController } from './agent-workspace-controller.js';
|
|
12
|
+
import { createHooksLifecycleEmitter } from './hooks-lifecycle.js';
|
|
13
|
+
import { createJitsiAgentBridge } from './jitsi-agent-bridge.js';
|
|
14
|
+
|
|
15
|
+
const MODEL_RATES = {
|
|
16
|
+
'claude-sonnet-4-20250514': { inputPer1k: 0.003, outputPer1k: 0.015 },
|
|
17
|
+
'claude-opus-4-20250514': { inputPer1k: 0.015, outputPer1k: 0.075 },
|
|
18
|
+
'claude-haiku-4-20250514': { inputPer1k: 0.0008, outputPer1k: 0.004 },
|
|
19
|
+
'gpt-4o': { inputPer1k: 0.005, outputPer1k: 0.015 },
|
|
20
|
+
'gpt-4o-mini': { inputPer1k: 0.00015, outputPer1k: 0.0006 },
|
|
21
|
+
'gemini-1.5-pro': { inputPer1k: 0.00125, outputPer1k: 0.005 },
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_RATE = { inputPer1k: 0.003, outputPer1k: 0.015 };
|
|
24
|
+
|
|
25
|
+
function estimateCost(event) {
|
|
26
|
+
if (!event?.usage) return 0;
|
|
27
|
+
const model = event.model || 'claude-sonnet-4-20250514';
|
|
28
|
+
const rate = MODEL_RATES[model] || DEFAULT_RATE;
|
|
29
|
+
const inputCost = ((event.usage.inputTokens || 0) / 1000) * rate.inputPer1k;
|
|
30
|
+
const outputCost = ((event.usage.outputTokens || 0) / 1000) * rate.outputPer1k;
|
|
31
|
+
return inputCost + outputCost;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const AGENT_DISPATCH_CONTROLLER_BOUNDARY = {
|
|
35
|
+
role: 'agent-dispatch-controller',
|
|
36
|
+
scope: 'Manual dispatch orchestration with permission gating, context assembly, workspace provisioning, stack resolution, K8s Job dispatch, and event persistence',
|
|
37
|
+
owns: ['dispatch creation', 'attempt lifecycle', 'K8s Job dispatch binding', 'workspace provisioning', 'stack resolution', 'session event persistence'],
|
|
38
|
+
delegatesTo: ['agent-permission-review', 'agent-stack-controller', 'agent-context-bundles', 'agent-mux-client', 'agent-memory-controller', 'agent-approval-controller', 'agent-workspace-controller'],
|
|
39
|
+
mustNotOwn: ['secret values', 'UI rendering']
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function createAgentDispatchController(options = {}) {
|
|
43
|
+
const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
|
|
44
|
+
const stackController = options.stackController || createAgentStackController();
|
|
45
|
+
const personaController = options.personaController || createAgentPersonaController();
|
|
46
|
+
const agentMuxClient = options.agentMuxClient || createAgentMuxClient();
|
|
47
|
+
const memoryController = options.memoryController || createAgentMemoryController();
|
|
48
|
+
const approvalController = options.approvalController || createAgentApprovalController();
|
|
49
|
+
const workspaceController = options.workspaceController || createAgentWorkspaceController();
|
|
50
|
+
const eventBus = options.eventBus || null;
|
|
51
|
+
const lifecycleEmitter = options.lifecycleEmitter || (eventBus ? createHooksLifecycleEmitter(eventBus) : null);
|
|
52
|
+
const hookBridge = options.hookBridge || null;
|
|
53
|
+
const jitsiAgentBridge = options.jitsiAgentBridge || createJitsiAgentBridge({
|
|
54
|
+
meetingController: options.jitsiMeetingController,
|
|
55
|
+
eventBus,
|
|
56
|
+
});
|
|
57
|
+
const logger = options.logger || console;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
role: 'agent-dispatch-controller',
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve an AgentStack CRD into a concrete execution config for agent-mux.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} stack - AgentStack resource
|
|
66
|
+
* @param {{ organizationRef?: string }} opts
|
|
67
|
+
* @returns {{ adapter: string, provider: string, model: string, prompt: object, mcpServers: string[], skills: string[], approvalMode: string, env: Record<string,string> }}
|
|
68
|
+
*/
|
|
69
|
+
resolveStack(stack, { organizationRef = 'default', identity = null } = {}) {
|
|
70
|
+
if (!stack || !stack.spec) {
|
|
71
|
+
throw new Error('resolveStack requires a valid AgentStack resource with spec');
|
|
72
|
+
}
|
|
73
|
+
const spec = stack.spec;
|
|
74
|
+
|
|
75
|
+
// Merge flat mcpServerRefs with structured externalTools.mcpServerRefs (deduplicated)
|
|
76
|
+
const mergedMcpServers = [
|
|
77
|
+
...(spec.mcpServerRefs || []),
|
|
78
|
+
...(spec.externalTools?.mcpServerRefs || [])
|
|
79
|
+
].filter((v, i, a) => a.indexOf(v) === i);
|
|
80
|
+
|
|
81
|
+
const memoryConfig = {};
|
|
82
|
+
if (spec.memoryRepositoryRefs && spec.memoryRepositoryRefs.length > 0) {
|
|
83
|
+
memoryConfig.memoryRepositoryRefs = clone(spec.memoryRepositoryRefs);
|
|
84
|
+
}
|
|
85
|
+
if (spec.memorySnapshotRef) {
|
|
86
|
+
memoryConfig.memorySnapshotRef = spec.memorySnapshotRef;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prompt = identity
|
|
90
|
+
? composeAgentPrompt({ ...identity, stack })
|
|
91
|
+
: {
|
|
92
|
+
system: spec.systemPrompt || null,
|
|
93
|
+
developer: spec.developerPrompt || null,
|
|
94
|
+
task: spec.taskPrompt || null,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
adapter: spec.adapter || spec.baseAgent || 'claude-code',
|
|
99
|
+
provider: spec.provider || 'anthropic',
|
|
100
|
+
model: spec.model || 'claude-sonnet-4-20250514',
|
|
101
|
+
prompt,
|
|
102
|
+
mcpServers: clone(mergedMcpServers),
|
|
103
|
+
skills: clone(spec.skillRefs || []),
|
|
104
|
+
approvalMode: spec.approvalMode || 'prompt',
|
|
105
|
+
env: {
|
|
106
|
+
KRADLE_ORG: organizationRef,
|
|
107
|
+
KRADLE_STACK_NAME: stack.metadata?.name || 'unknown',
|
|
108
|
+
},
|
|
109
|
+
memoryConfig: Object.keys(memoryConfig).length > 0 ? memoryConfig : null,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if the run has exceeded its token or cost budget.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} run - AgentDispatchRun resource
|
|
117
|
+
* @param {object} event - SSE event with optional usage field
|
|
118
|
+
* @returns {{ exceeded: boolean, reason?: string, current?: number, limit?: number, totalTokens?: number, totalCost?: number }}
|
|
119
|
+
*/
|
|
120
|
+
checkBudget(run, event) {
|
|
121
|
+
const maxTokens = run.spec?.budget?.maxTokens ?? Infinity;
|
|
122
|
+
const maxCostUsd = run.spec?.budget?.maxCostUsd ?? Infinity;
|
|
123
|
+
const currentTokens = run.status?.tokenUsage?.totalTokens || 0;
|
|
124
|
+
const eventTokens = event?.usage?.totalTokens
|
|
125
|
+
|| ((event?.usage?.inputTokens || 0) + (event?.usage?.outputTokens || 0));
|
|
126
|
+
const totalTokens = currentTokens + eventTokens;
|
|
127
|
+
const currentCost = run.status?.costUsd || 0;
|
|
128
|
+
const totalCost = currentCost + estimateCost(event);
|
|
129
|
+
|
|
130
|
+
if (totalTokens > maxTokens) {
|
|
131
|
+
return { exceeded: true, reason: 'token_limit', current: totalTokens, limit: maxTokens };
|
|
132
|
+
}
|
|
133
|
+
if (totalCost > maxCostUsd) {
|
|
134
|
+
return { exceeded: true, reason: 'cost_limit', current: totalCost, limit: maxCostUsd };
|
|
135
|
+
}
|
|
136
|
+
return { exceeded: false, totalTokens, totalCost };
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Persist a session event from an agent-mux session.
|
|
141
|
+
* Appends to the transcript, updates the dispatch attempt status,
|
|
142
|
+
* marks the run as Completed/Failed on terminal events, and emits to event bus.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} event - The SSE event object
|
|
145
|
+
* @param {object} run - AgentDispatchRun resource (mutated in place)
|
|
146
|
+
* @param {object} attempt - AgentDispatchAttempt resource (mutated in place)
|
|
147
|
+
* @param {{ namespace?: string, organizationRef?: string, transcript?: object }} opts
|
|
148
|
+
* @returns {{ run: object, attempt: object, transcript: object, notification: object|null }}
|
|
149
|
+
*/
|
|
150
|
+
persistSessionEvent(event, run, attempt, { namespace = 'default', organizationRef = 'default', transcript = null } = {}) {
|
|
151
|
+
if (!event || typeof event !== 'object') {
|
|
152
|
+
return { run, attempt, transcript, notification: null };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Budget enforcement — check before persisting
|
|
156
|
+
const budgetCheck = this.checkBudget(run, event);
|
|
157
|
+
if (budgetCheck.exceeded) {
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
run.status.phase = 'Failed';
|
|
160
|
+
run.status.failedAt = now;
|
|
161
|
+
run.status.failureReason = 'budget_exceeded';
|
|
162
|
+
run.status.budgetExceeded = { reason: budgetCheck.reason, current: budgetCheck.current, limit: budgetCheck.limit };
|
|
163
|
+
attempt.status.failedAt = now;
|
|
164
|
+
attempt.status.failureReason = 'budget_exceeded';
|
|
165
|
+
if (eventBus) {
|
|
166
|
+
eventBus.emit({ type: 'run-complete', status: 'failed', name: run.metadata?.name, reason: 'budget_exceeded', timestamp: now });
|
|
167
|
+
}
|
|
168
|
+
const budgetNotification = { type: 'run-complete', status: 'failed', name: run.metadata?.name, reason: 'budget_exceeded', timestamp: now };
|
|
169
|
+
return { run, attempt, transcript, notification: budgetNotification };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const now = new Date().toISOString();
|
|
173
|
+
|
|
174
|
+
// 1. Append to AgentSessionTranscript
|
|
175
|
+
if (!transcript) {
|
|
176
|
+
const sessionRef = attempt?.status?.agentMuxSessionId || 'unknown';
|
|
177
|
+
transcript = createResource('AgentSessionTranscript', { name: `transcript-${sessionRef}`, namespace }, {
|
|
178
|
+
organizationRef,
|
|
179
|
+
sessionRef,
|
|
180
|
+
messages: [],
|
|
181
|
+
cost: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
182
|
+
}, { phase: 'Streaming', startedAt: now });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const role = event.role || 'system';
|
|
186
|
+
const content = event.content || event.text || event.message || '';
|
|
187
|
+
const message = {
|
|
188
|
+
role,
|
|
189
|
+
content: typeof content === 'string' ? content : JSON.stringify(content),
|
|
190
|
+
timestamp: event.timestamp || now,
|
|
191
|
+
};
|
|
192
|
+
if (event.toolUse) message.toolUse = event.toolUse;
|
|
193
|
+
if (event.toolResult) message.toolResult = event.toolResult;
|
|
194
|
+
transcript.spec.messages.push(message);
|
|
195
|
+
|
|
196
|
+
// Accumulate token usage
|
|
197
|
+
if (event.usage) {
|
|
198
|
+
transcript.spec.cost.inputTokens += event.usage.inputTokens || 0;
|
|
199
|
+
transcript.spec.cost.outputTokens += event.usage.outputTokens || 0;
|
|
200
|
+
transcript.spec.cost.totalTokens = transcript.spec.cost.inputTokens + transcript.spec.cost.outputTokens;
|
|
201
|
+
|
|
202
|
+
if (!run.status.tokenUsage) run.status.tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
203
|
+
run.status.tokenUsage.inputTokens = (run.status.tokenUsage.inputTokens || 0) + (event.usage.inputTokens || 0);
|
|
204
|
+
run.status.tokenUsage.outputTokens = (run.status.tokenUsage.outputTokens || 0) + (event.usage.outputTokens || 0);
|
|
205
|
+
run.status.tokenUsage.totalTokens = run.status.tokenUsage.inputTokens + run.status.tokenUsage.outputTokens;
|
|
206
|
+
run.status.costUsd = (run.status.costUsd || 0) + estimateCost(event);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Update AgentDispatchAttempt status
|
|
210
|
+
attempt.status.lastEventAt = now;
|
|
211
|
+
attempt.status.eventCount = (attempt.status.eventCount || 0) + 1;
|
|
212
|
+
|
|
213
|
+
// 3. Terminal event handling
|
|
214
|
+
let notification = null;
|
|
215
|
+
if (event.type === 'completion') {
|
|
216
|
+
run.status.phase = 'Completed';
|
|
217
|
+
run.status.completedAt = now;
|
|
218
|
+
attempt.status.completedAt = now;
|
|
219
|
+
transcript.status.phase = 'Reconciled';
|
|
220
|
+
transcript.status.reconciledAt = now;
|
|
221
|
+
|
|
222
|
+
notification = {
|
|
223
|
+
type: 'run-complete',
|
|
224
|
+
status: 'completed',
|
|
225
|
+
name: run.metadata?.name,
|
|
226
|
+
org: organizationRef,
|
|
227
|
+
timestamp: now,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (lifecycleEmitter) {
|
|
231
|
+
lifecycleEmitter.emitRunCompleted(run, { phase: 'Completed' });
|
|
232
|
+
lifecycleEmitter.emitSessionEnded({ sessionId: attempt.status?.agentMuxSessionId, runId: attempt.status?.agentMuxRunId });
|
|
233
|
+
}
|
|
234
|
+
} else if (event.type === 'error') {
|
|
235
|
+
run.status.phase = 'Failed';
|
|
236
|
+
run.status.failedAt = now;
|
|
237
|
+
run.status.failureReason = event.error || event.message || 'Unknown error';
|
|
238
|
+
attempt.status.failedAt = now;
|
|
239
|
+
attempt.status.failureReason = event.error || event.message || 'Unknown error';
|
|
240
|
+
transcript.status.phase = 'Failed';
|
|
241
|
+
|
|
242
|
+
notification = {
|
|
243
|
+
type: 'run-complete',
|
|
244
|
+
status: 'failed',
|
|
245
|
+
name: run.metadata?.name,
|
|
246
|
+
org: organizationRef,
|
|
247
|
+
timestamp: now,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (lifecycleEmitter) {
|
|
251
|
+
lifecycleEmitter.emitRunCompleted(run, { phase: 'Failed' });
|
|
252
|
+
lifecycleEmitter.emitSessionEnded({ sessionId: attempt.status?.agentMuxSessionId, runId: attempt.status?.agentMuxRunId });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 5. Emit to event bus for SSE broadcast
|
|
257
|
+
if (eventBus) {
|
|
258
|
+
eventBus.emit({
|
|
259
|
+
type: 'session-event',
|
|
260
|
+
runName: run.metadata?.name,
|
|
261
|
+
eventType: event.type || 'message',
|
|
262
|
+
timestamp: now,
|
|
263
|
+
});
|
|
264
|
+
if (notification) {
|
|
265
|
+
eventBus.emit(notification);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { run, attempt, transcript, notification };
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
async createManualDispatch({ repository, ref, sourceRefs = [], agentDefinition, agentStack, meetingRef, taskKind, actor, namespace = 'default', organizationRef = 'default', resources = {}, callbackUrl } = {}, options = {}) {
|
|
273
|
+
let identity = null;
|
|
274
|
+
if (agentDefinition) {
|
|
275
|
+
const resolved = personaController.resolveAgentDefinition(agentDefinition, { resources, organizationRef });
|
|
276
|
+
if (resolved.error) {
|
|
277
|
+
return { error: true, reason: 'agent-definition-invalid', message: resolved.errors.join('; '), errors: resolved.errors };
|
|
278
|
+
}
|
|
279
|
+
identity = {
|
|
280
|
+
definition: resolved.definition,
|
|
281
|
+
persona: resolved.persona,
|
|
282
|
+
soul: resolved.soul,
|
|
283
|
+
appearance: resolved.appearance,
|
|
284
|
+
voiceProfile: resolved.voiceProfile,
|
|
285
|
+
};
|
|
286
|
+
agentStack = resolved.stack.metadata?.name;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!agentStack) {
|
|
290
|
+
return { error: true, reason: 'dispatch-target-required', message: 'Dispatch requires agentDefinition or agentStack' };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 0. Virtual model PreModelResolution hook — can redirect to a different model/stack
|
|
294
|
+
if (hookBridge) {
|
|
295
|
+
const virtualModels = resources.KradleVirtualModel || [];
|
|
296
|
+
const matchedVm = hookBridge.matchVirtualModel(agentStack, virtualModels);
|
|
297
|
+
if (matchedVm) {
|
|
298
|
+
const preResult = hookBridge.handleHook('VirtualModel.PreModelResolution', {
|
|
299
|
+
modelId: agentStack, repository, actor, taskKind
|
|
300
|
+
}, matchedVm);
|
|
301
|
+
if (preResult.decision === 'deny') {
|
|
302
|
+
return { error: true, reason: 'virtual-model-denied', message: preResult.message || 'Blocked by virtual model policy' };
|
|
303
|
+
}
|
|
304
|
+
if (preResult.decision === 'modify' && preResult.modifiedInput?.modelId) {
|
|
305
|
+
agentStack = preResult.modifiedInput.modelId;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 1. Find stack
|
|
311
|
+
const stack = (resources.AgentStack || []).find(s => s.metadata?.name === agentStack);
|
|
312
|
+
if (!stack) return { error: true, reason: 'stack-not-found', message: `AgentStack '${agentStack}' not found` };
|
|
313
|
+
if (meetingRef && !jitsiAgentBridge.hasMeetingCapability(stack)) {
|
|
314
|
+
return { error: true, reason: 'meeting-not-supported', message: `AgentStack '${agentStack}' is not Jitsi-capable` };
|
|
315
|
+
}
|
|
316
|
+
const legacyWarning = legacyAgentStackIdentityWarning(stack);
|
|
317
|
+
if (legacyWarning && typeof logger?.warn === 'function') logger.warn(legacyWarning);
|
|
318
|
+
|
|
319
|
+
// 2. Permission review
|
|
320
|
+
const review = permissionReviewer.reviewPermissions({ repository, ref, actor, agentStack, resources });
|
|
321
|
+
if (review.decision === 'denied') {
|
|
322
|
+
return { error: true, reason: 'permission-denied', message: 'Dispatch denied by permission review', review };
|
|
323
|
+
}
|
|
324
|
+
const permissionSnapshot = permissionReviewer.createPermissionSnapshot(review);
|
|
325
|
+
|
|
326
|
+
// 3. Memory snapshot — use stack-scoped memoryRepositoryRefs if present, else fall back to all repos
|
|
327
|
+
let memorySnapshot = null;
|
|
328
|
+
const allMemoryRepos = resources.AgentMemoryRepository || [];
|
|
329
|
+
const stackMemoryRefs = stack.spec?.memoryRepositoryRefs || [];
|
|
330
|
+
const scopedMemoryRepos = stackMemoryRefs.length > 0
|
|
331
|
+
? allMemoryRepos.filter((r) => stackMemoryRefs.includes(r.metadata?.name))
|
|
332
|
+
: allMemoryRepos;
|
|
333
|
+
if (scopedMemoryRepos.length > 0) {
|
|
334
|
+
const memRepo = scopedMemoryRepos[0];
|
|
335
|
+
const timeTravel = memoryController.resolveTimeTravel({ mode: 'current', commits: [] });
|
|
336
|
+
memorySnapshot = memoryController.createMemorySnapshot({
|
|
337
|
+
memoryRepository: memRepo.metadata.name,
|
|
338
|
+
requestedRef: ref,
|
|
339
|
+
resolvedCommit: timeTravel.resolvedCommit || ref,
|
|
340
|
+
queryManifest: {},
|
|
341
|
+
selectedRecords: [],
|
|
342
|
+
selectedDocuments: [],
|
|
343
|
+
ontologyDigest: '',
|
|
344
|
+
namespace,
|
|
345
|
+
organizationRef,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 4. Approval gate — if review requires approval, create approval and return early
|
|
350
|
+
if (review.decision === 'requires-approval') {
|
|
351
|
+
const now = new Date().toISOString();
|
|
352
|
+
const runName = `dispatch-${Date.now()}`;
|
|
353
|
+
|
|
354
|
+
const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
|
|
355
|
+
organizationRef,
|
|
356
|
+
repository,
|
|
357
|
+
sourceRefs: clone(sourceRefs),
|
|
358
|
+
...(agentDefinition ? { agentDefinition } : {}),
|
|
359
|
+
agentStack,
|
|
360
|
+
taskKind: taskKind || 'diagnostic',
|
|
361
|
+
contextBundleRef: null,
|
|
362
|
+
});
|
|
363
|
+
run.status = { phase: 'AwaitingApproval', queuedAt: now };
|
|
364
|
+
if (memorySnapshot) {
|
|
365
|
+
run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const approvalResult = approvalController.createApprovalRequest({
|
|
369
|
+
dispatchRun: runName,
|
|
370
|
+
action: 'secret-access',
|
|
371
|
+
requestedBy: actor,
|
|
372
|
+
context: `Dispatch requires approval for agent stack: ${agentStack}`,
|
|
373
|
+
namespace,
|
|
374
|
+
organizationRef,
|
|
375
|
+
resources,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
error: false,
|
|
380
|
+
run,
|
|
381
|
+
approval: approvalResult.error ? null : approvalResult.approval,
|
|
382
|
+
awaitingApproval: true,
|
|
383
|
+
memorySnapshot,
|
|
384
|
+
permissionSnapshot,
|
|
385
|
+
review,
|
|
386
|
+
warnings: legacyWarning ? [legacyWarning] : [],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 5. Workspace provisioning — reuse or create
|
|
391
|
+
let workspaceResult = null;
|
|
392
|
+
let mountSpec = null;
|
|
393
|
+
const branch = ref || 'main';
|
|
394
|
+
|
|
395
|
+
const reusable = workspaceController.findReusableWorkspace({
|
|
396
|
+
organizationRef, repository, branch, resources,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (reusable) {
|
|
400
|
+
const claimResult = workspaceController.claimWorkspace({
|
|
401
|
+
name: reusable.metadata.name,
|
|
402
|
+
runRef: `dispatch-pending`,
|
|
403
|
+
resources,
|
|
404
|
+
});
|
|
405
|
+
if (!claimResult.error) {
|
|
406
|
+
workspaceResult = { workspace: claimResult.workspace, reused: true };
|
|
407
|
+
const mount = workspaceController.getMountSpec({ workspace: claimResult.workspace });
|
|
408
|
+
if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!workspaceResult) {
|
|
413
|
+
const createResult = workspaceController.createWorkspace({
|
|
414
|
+
organizationRef, repository, branch, namespace,
|
|
415
|
+
volumeSpec: {},
|
|
416
|
+
});
|
|
417
|
+
if (!createResult.error) {
|
|
418
|
+
workspaceResult = { workspace: createResult.workspace, pvcManifest: createResult.pvcManifest, reused: false };
|
|
419
|
+
const mount = workspaceController.getMountSpec({ workspace: createResult.workspace });
|
|
420
|
+
if (!mount.error) mountSpec = { volume: mount.volume, volumeMount: mount.volumeMount };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 6. Assemble context bundle
|
|
425
|
+
const contextBundle = assembleContextBundle({ stack, repository, ref, sourceRefs, contextLabels: [], resources });
|
|
426
|
+
|
|
427
|
+
// 7. Create resources
|
|
428
|
+
const now = new Date().toISOString();
|
|
429
|
+
const runName = `dispatch-${Date.now()}`;
|
|
430
|
+
|
|
431
|
+
const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
|
|
432
|
+
organizationRef,
|
|
433
|
+
repository,
|
|
434
|
+
sourceRefs: clone(sourceRefs),
|
|
435
|
+
...(agentDefinition ? { agentDefinition } : {}),
|
|
436
|
+
agentStack,
|
|
437
|
+
taskKind: taskKind || 'diagnostic',
|
|
438
|
+
contextBundleRef: contextBundle.metadata.name,
|
|
439
|
+
});
|
|
440
|
+
run.status = { phase: 'Pending', queuedAt: now };
|
|
441
|
+
if (memorySnapshot) {
|
|
442
|
+
run.spec.memorySnapshotRef = memorySnapshot.metadata.name;
|
|
443
|
+
}
|
|
444
|
+
if (workspaceResult) {
|
|
445
|
+
run.spec.workspaceRef = workspaceResult.workspace.metadata.name;
|
|
446
|
+
}
|
|
447
|
+
if (mountSpec) {
|
|
448
|
+
run.spec.mountSpec = mountSpec;
|
|
449
|
+
}
|
|
450
|
+
let runtimeMeetingContext = null;
|
|
451
|
+
if (meetingRef) {
|
|
452
|
+
try {
|
|
453
|
+
runtimeMeetingContext = await jitsiAgentBridge.prepareMeetingContext(run, meetingRef, stack, { resources, organizationRef, namespace });
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return { error: true, reason: 'meeting-unavailable', message: err.message || `Meeting ${meetingRef} is not available` };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (lifecycleEmitter) {
|
|
460
|
+
lifecycleEmitter.emitRunCreated(run);
|
|
461
|
+
if (workspaceResult) {
|
|
462
|
+
lifecycleEmitter.emitWorkspaceProvisioned(workspaceResult.workspace);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Update workspace runRef to actual dispatch name
|
|
467
|
+
if (workspaceResult) {
|
|
468
|
+
workspaceResult.workspace.status.runRef = runName;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const attempt = createResource('AgentDispatchAttempt', { name: `${runName}-attempt-1`, namespace }, {
|
|
472
|
+
organizationRef,
|
|
473
|
+
agentDispatchRun: runName,
|
|
474
|
+
attemptReason: 'initial',
|
|
475
|
+
agentStackSnapshot: clone(stack.spec),
|
|
476
|
+
...(identity?.definition ? { agentDefinitionSnapshot: clone(identity.definition.spec) } : {}),
|
|
477
|
+
...(identity?.persona ? { agentPersonaSnapshot: clone(identity.persona.spec) } : {}),
|
|
478
|
+
contextBundleDigest: contextBundle.spec.digest,
|
|
479
|
+
});
|
|
480
|
+
attempt.status = { permissionSnapshot, queueEnteredAt: now };
|
|
481
|
+
|
|
482
|
+
// 7. Dispatch as K8s Job
|
|
483
|
+
let transcript = null;
|
|
484
|
+
let jobResult = null;
|
|
485
|
+
|
|
486
|
+
const executionConfig = this.resolveStack(stack, { organizationRef, identity });
|
|
487
|
+
const pvcName = workspaceResult?.workspace?.spec?.volumeClaimName || workspaceResult?.pvcManifest?.metadata?.name || null;
|
|
488
|
+
const resolvedCallbackUrl = callbackUrl || process.env.KRADLE_CALLBACK_URL || null;
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
// Inject memory config env vars into the Job when memory repos are scoped
|
|
492
|
+
const jobEnv = { ...executionConfig.env };
|
|
493
|
+
if (executionConfig.memoryConfig) {
|
|
494
|
+
if (executionConfig.memoryConfig.memoryRepositoryRefs) {
|
|
495
|
+
jobEnv.KRADLE_MEMORY_REPOS = executionConfig.memoryConfig.memoryRepositoryRefs.join(',');
|
|
496
|
+
}
|
|
497
|
+
if (executionConfig.memoryConfig.memorySnapshotRef) {
|
|
498
|
+
jobEnv.KRADLE_MEMORY_SNAPSHOT = executionConfig.memoryConfig.memorySnapshotRef;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const { jobManifest, jobName } = agentMuxClient.createAgentJob({
|
|
503
|
+
adapter: executionConfig.adapter,
|
|
504
|
+
provider: executionConfig.provider,
|
|
505
|
+
model: executionConfig.model,
|
|
506
|
+
org: organizationRef,
|
|
507
|
+
runId: runName,
|
|
508
|
+
stackName: stack.metadata?.name,
|
|
509
|
+
budget: stack.spec?.budget,
|
|
510
|
+
image: stack.spec?.image,
|
|
511
|
+
serviceAccount: stack.spec?.runtimeIdentity?.serviceAccountRef,
|
|
512
|
+
callbackUrl: resolvedCallbackUrl,
|
|
513
|
+
prompt: executionConfig.prompt,
|
|
514
|
+
env: jobEnv,
|
|
515
|
+
workspace: pvcName ? { pvcName } : undefined,
|
|
516
|
+
resources: stack.spec?.resources,
|
|
517
|
+
meetingContext: runtimeMeetingContext || run.spec.meetingContext,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
attempt.status.jobName = jobName;
|
|
521
|
+
attempt.status.jobNamespace = jobManifest.metadata.namespace;
|
|
522
|
+
run.spec.jobRef = jobName;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const submitResult = await agentMuxClient.submitAgentJob(jobManifest);
|
|
526
|
+
run.status.phase = 'Running';
|
|
527
|
+
attempt.status.startedAt = now;
|
|
528
|
+
attempt.status.jobSubmitted = true;
|
|
529
|
+
jobResult = { jobName: submitResult.jobName, namespace: submitResult.namespace, submitted: true };
|
|
530
|
+
|
|
531
|
+
if (lifecycleEmitter) {
|
|
532
|
+
lifecycleEmitter.emitSessionStarted({ sessionId: jobName, runId: runName });
|
|
533
|
+
lifecycleEmitter.emitStepStarted(run, 'launch');
|
|
534
|
+
}
|
|
535
|
+
} catch {
|
|
536
|
+
// Job manifest was generated but submission failed — queue for retry
|
|
537
|
+
run.status.phase = 'Queued';
|
|
538
|
+
run.status.conditions = [{ type: 'JobSubmitted', status: 'False', reason: 'SubmitFailed', message: 'K8s Job submission failed' }];
|
|
539
|
+
jobResult = { jobName, namespace: jobManifest.metadata.namespace, submitted: false };
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
run.status.phase = 'Queued';
|
|
543
|
+
run.status.conditions = [{ type: 'JobSubmitted', status: 'False', reason: 'ManifestFailed', message: err.message || 'Job manifest generation failed' }];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { error: false, run, attempt, contextBundle, permissionSnapshot, memorySnapshot, transcript, workspace: workspaceResult, mountSpec, jobResult, executionConfig, identity, warnings: legacyWarning ? [legacyWarning] : [] };
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Agent Gateway Config Controller — Slice 1.2e
|
|
2
|
+
// Manages AgentGatewayConfig resources: gateway name, endpoint URL, feature flags,
|
|
3
|
+
// connection pool settings, and TLS configuration reference.
|
|
4
|
+
|
|
5
|
+
export const AGENT_GATEWAY_CONFIG_CONTROLLER_BOUNDARY = {
|
|
6
|
+
role: 'agent-gateway-config-controller',
|
|
7
|
+
scope: 'AgentGatewayConfig lifecycle: validation, endpoint resolution, feature flags, connection pool, TLS config ref',
|
|
8
|
+
owns: ['gateway config validation', 'endpoint URL', 'feature flags', 'connection pool defaults', 'TLS config ref'],
|
|
9
|
+
delegatesTo: ['resource-model'],
|
|
10
|
+
mustNotOwn: ['secret values', 'dispatch execution', 'Agent Mux sessions', 'adapter implementation']
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FEATURE_FLAGS = Object.freeze({
|
|
14
|
+
streaming: true,
|
|
15
|
+
reconnect: true,
|
|
16
|
+
healthCheck: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const DEFAULT_POOL_SETTINGS = Object.freeze({
|
|
20
|
+
maxConnections: 10,
|
|
21
|
+
timeoutMs: 30000
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate an AgentGatewayConfig resource. Returns { valid, errors }.
|
|
26
|
+
* @param {object} resource
|
|
27
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
28
|
+
*/
|
|
29
|
+
export function validateAgentGatewayConfig(resource) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
|
|
32
|
+
// Guard against null/undefined resource
|
|
33
|
+
if (resource == null) {
|
|
34
|
+
errors.push('resource must not be null or undefined');
|
|
35
|
+
return { valid: false, errors };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate metadata.name
|
|
39
|
+
if (!resource?.metadata?.name) {
|
|
40
|
+
errors.push('metadata.name is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spec = resource?.spec || {};
|
|
44
|
+
|
|
45
|
+
// Validate organizationRef
|
|
46
|
+
if (!spec.organizationRef) {
|
|
47
|
+
errors.push('spec.organizationRef is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate endpoint URL
|
|
51
|
+
if (!spec.endpointUrl) {
|
|
52
|
+
errors.push('spec.endpointUrl is required; provide the Agent Mux gateway endpoint URL');
|
|
53
|
+
} else {
|
|
54
|
+
// Basic URL validation: must start with http:// or https:// or ws:// or wss://
|
|
55
|
+
const validProtocols = ['http://', 'https://', 'ws://', 'wss://'];
|
|
56
|
+
const hasValidProtocol = validProtocols.some((p) => spec.endpointUrl.startsWith(p));
|
|
57
|
+
if (!hasValidProtocol) {
|
|
58
|
+
errors.push(`spec.endpointUrl must start with one of: ${validProtocols.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate connectionPool if provided
|
|
63
|
+
const pool = spec.connectionPool;
|
|
64
|
+
if (pool != null) {
|
|
65
|
+
if (pool.maxConnections != null && (!Number.isInteger(pool.maxConnections) || pool.maxConnections < 1)) {
|
|
66
|
+
errors.push('spec.connectionPool.maxConnections must be a positive integer');
|
|
67
|
+
}
|
|
68
|
+
if (pool.timeoutMs != null && (!Number.isFinite(pool.timeoutMs) || pool.timeoutMs < 0)) {
|
|
69
|
+
errors.push('spec.connectionPool.timeoutMs must be a non-negative number');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { valid: errors.length === 0, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Factory that returns an AgentGatewayConfig controller instance.
|
|
78
|
+
*/
|
|
79
|
+
export function createAgentGatewayConfigController() {
|
|
80
|
+
return {
|
|
81
|
+
role: 'agent-gateway-config-controller',
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate an AgentGatewayConfig resource.
|
|
85
|
+
* @param {object} resource
|
|
86
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
87
|
+
*/
|
|
88
|
+
validate(resource) {
|
|
89
|
+
return validateAgentGatewayConfig(resource);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the effective endpoint URL for the gateway config.
|
|
94
|
+
* @param {object} resource
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
getEndpointUrl(resource) {
|
|
98
|
+
if (resource == null) {
|
|
99
|
+
throw new Error('resource must not be null or undefined');
|
|
100
|
+
}
|
|
101
|
+
return resource?.spec?.endpointUrl ?? null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return the effective feature flags for a gateway config.
|
|
106
|
+
* Merges spec.featureFlags with defaults; spec values take precedence.
|
|
107
|
+
* @param {object} resource
|
|
108
|
+
* @returns {{ streaming: boolean, reconnect: boolean, healthCheck: boolean, [key: string]: boolean }}
|
|
109
|
+
*/
|
|
110
|
+
getFeatureFlags(resource) {
|
|
111
|
+
if (resource == null) {
|
|
112
|
+
throw new Error('resource must not be null or undefined');
|
|
113
|
+
}
|
|
114
|
+
const specFlags = resource?.spec?.featureFlags ?? {};
|
|
115
|
+
return { ...DEFAULT_FEATURE_FLAGS, ...specFlags };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Return the effective connection pool settings for a gateway config.
|
|
120
|
+
* Merges spec.connectionPool with defaults; spec values take precedence.
|
|
121
|
+
* @param {object} resource
|
|
122
|
+
* @returns {{ maxConnections: number, timeoutMs: number }}
|
|
123
|
+
*/
|
|
124
|
+
getConnectionPool(resource) {
|
|
125
|
+
if (resource == null) {
|
|
126
|
+
throw new Error('resource must not be null or undefined');
|
|
127
|
+
}
|
|
128
|
+
const specPool = resource?.spec?.connectionPool ?? {};
|
|
129
|
+
return {
|
|
130
|
+
maxConnections: specPool.maxConnections ?? DEFAULT_POOL_SETTINGS.maxConnections,
|
|
131
|
+
timeoutMs: specPool.timeoutMs ?? DEFAULT_POOL_SETTINGS.timeoutMs
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Return the TLS configuration reference from the spec, or null if not set.
|
|
137
|
+
* @param {object} resource
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
getTlsConfigRef(resource) {
|
|
141
|
+
if (resource == null) {
|
|
142
|
+
throw new Error('resource must not be null or undefined');
|
|
143
|
+
}
|
|
144
|
+
return resource?.spec?.tlsConfigRef ?? null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|