Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Install dependencies
timeout-minutes: 20
run: npm ci --ignore-scripts --workspaces=false --no-audit --no-fund
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@641fdc5a45d76d30ffa52cd51752043846bb3d44
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@5bf0a7bc3e9090f0885abb915101d33fe5d28df8
with:
github-token: ${{ github.token }}
paths: |
Expand Down
18 changes: 18 additions & 0 deletions drizzle/control-plane/0034_recurring_workday_canonical_intent.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ALTER TABLE "capacity_workday_schedules" ADD COLUMN "intent_json" text;
--> statement-breakpoint
UPDATE "capacity_workday_schedules" SET "intent_json" = jsonb_strip_nulls(jsonb_build_object(
'schemaVersion', 'treeseed.workday-intent/v1', 'teamId', "team_id", 'profileId', 'default',
'projects', "project_ids_json"::jsonb, 'startsAt', "next_run_at", 'durationSeconds', "duration_seconds",
'planningOnly', "planning_only" = 1,
'agentSelection', (SELECT jsonb_object_agg(key, value) FROM jsonb_each("agent_selection_json"::jsonb)
WHERE key IN ('agentSlugs', 'activityTypes', 'classIds', 'classSlugs')
AND jsonb_typeof(value) = 'array' AND value <> '[]'::jsonb),
'operatorConstraints', jsonb_build_object('providerIds', jsonb_build_array("capacity_provider_id"), 'maxConcurrency', "max_active_assignments")
))::text;
--> statement-breakpoint
ALTER TABLE "capacity_workday_schedules" ALTER COLUMN "intent_json" SET NOT NULL;
--> statement-breakpoint
ALTER TABLE "capacity_workday_schedules"
DROP COLUMN "capacity_provider_id", DROP COLUMN "project_ids_json", DROP COLUMN "agent_selection_json",
DROP COLUMN "duration_seconds", DROP COLUMN "max_active_assignments", DROP COLUMN "available_seconds",
DROP COLUMN "time_policy_json", DROP COLUMN "planning_only", DROP COLUMN "publication_policy_json";
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export interface CreateWorkdayCapacityEnvelopeInput {
completedAt?: string | null;
environment?: string | null;
availableSeconds?: number | null;
timePolicy?: JsonRecord;
envelope?: JsonRecord;
metadata?: JsonRecord;
}
Expand Down Expand Up @@ -105,7 +104,7 @@ export class WorkdayCapacityEnvelopeRepository {
teamId: text(project.team_id), projectId: text(project.id), workDayId: id,
environment: input.environment ?? null,
allocationSetId: input.allocationSetId ?? nullableText(suppliedEnvelope.allocationSetId),
availableSeconds: amount(input.availableSeconds, null), timePolicy: object(input.timePolicy),
availableSeconds: amount(input.availableSeconds, null),
metadata: object(suppliedEnvelope.metadata ?? metadata),
...suppliedEnvelope,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { CapacityGovernanceDatabase } from '../../../../database.ts';
import type { DurableCapacityWorkdayRun } from '../../../../repositories/capacity/workdays/workday-run.ts';
import type { ProviderSynthesisExecutionProvider } from '../../providers/provider-synthesis-context-service.ts';

export type LivingAllocationInputs = Record<string, { measurements: AllocationMeasurement[]; constraints: AssignmentAllocationConstraint[] }>;
export type LivingAllocationInputs = Record<string, { measurements: AllocationMeasurement[]; constraints: AssignmentAllocationConstraint[];
opportunity: ReturnType<typeof allocateWorkdayCapacity>[string] }>;

/** Read existing graph/reservation/usage authority; no performance or allocation store. */
export async function livingAllocationInputs(store: CapacityGovernanceDatabase, input: {
Expand Down Expand Up @@ -66,7 +67,7 @@ export async function livingAllocationInputs(store: CapacityGovernanceDatabase,
OR (assignment.status='failed' AND assignment.lifecycle_code='assignment_timeout'))
ORDER BY usage.created_at DESC,usage.id DESC LIMIT 20`,
[input.capacityProviderId, provider.id, input.agentClass, limits.modelConfigurationId, input.capabilityId, input.activity]);
result[provider.id] = { constraints: [{ id: 'workday-phase-share', remainingSeconds: shares[input.run.id]?.availableSeconds ?? 0 }],
result[provider.id] = { opportunity: shares[input.run.id]!, constraints: [{ id: 'workday-phase-share', remainingSeconds: shares[input.run.id]?.availableSeconds ?? 0 }],
measurements: rows.map(row => ({ id: String(row.id), completedAt: String(row.created_at), expectedSeconds: Number(row.expected_seconds),
allocatedSeconds: Number(row.allocated_seconds), activeSeconds: Number(row.active_seconds), outcome: row.lifecycle_code === 'assignment_timeout' ? 'expired' : 'completed' })) };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AssignmentAttempt, CapabilityAccountingLimits, calculateAssignmentAllocation } from '@treeseed/sdk/agent-capacity';
import type { AssignmentAttempt, CapabilityAccountingLimits, calculateAssignmentAllocation, allocateWorkdayCapacity, selectFairReadyNode } from '@treeseed/sdk/agent-capacity';
import { randomUUID } from 'node:crypto';
import { capabilityCounterClaims, initializeCapabilityCounters, commitCapabilityCounters } from './capability-counter-claims.ts';
import type { CapacityGovernanceDatabase } from '../../../../database.ts';
Expand All @@ -17,7 +17,8 @@ type JsonRecord = Record<string, unknown>;
export async function admitLivingExecutionAssignment(store: Store, input: {
principal: ProviderLeasePrincipal;
assignment: AssignmentAttempt;
allocation: ReturnType<typeof calculateAssignmentAllocation>;
allocation: ReturnType<typeof calculateAssignmentAllocation> & {
opportunity: ReturnType<typeof allocateWorkdayCapacity>[string]; selection: ReturnType<typeof selectFairReadyNode> };
accountingLimits: CapabilityAccountingLimits;
projectAgentClassId: string;
providerSessionId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,6 @@ export function buildAssignmentAttempt(input: {
deadline, leaseId: id('lease', [assignmentId]), reservationId: id('reservation', [assignmentId]),
attempt: input.attempt, status: 'created', createdAt: input.now,
});
return { assignment, allocation, accountingLimits: limits, executionProviderId: selected.provider.id, laneId: selected.lane.id,
return { assignment, allocation: { ...allocation, opportunity: allocationInputs.opportunity }, accountingLimits: limits, executionProviderId: selected.provider.id, laneId: selected.lane.id,
lanePurpose: communication ? 'communication' : 'workday' };
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export async function assignNextReadyExecutionNode(
});
const attempt = selected.assignment;
const treedxProxyHandle = await issueLivingTreeDxAuthority(store, run, attempt, now);
return await admitLivingExecutionAssignment(store, { principal, assignment: attempt, allocation: selected.allocation,
return await admitLivingExecutionAssignment(store, { principal, assignment: attempt, allocation: { ...selected.allocation, selection: selectedNode },
accountingLimits: selected.accountingLimits,
projectAgentClassId: candidate.projectAgentClassId, providerSessionId,
executionProviderId: selected.executionProviderId, laneId: selected.laneId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function digest(value:unknown):string { return `sha256:${sha256(canonicalJson(va
function diagnosticsError(code:string,message:string,diagnostics:unknown):never { throw new CapacityGovernanceError(code,message,400,{diagnostics}); }

export function parsePublicWorkdayIntent(teamId:string,input:JsonRecord):WorkdayIntent {
const allowed=new Set(['schemaVersion','teamId','profileId','projects','startsAt','endsAt','durationSeconds','objectiveFilters','planningOnly','proposalIds','decisionIds','operatorConstraints','agentSelection','allocation']);
const allowed=new Set(['schemaVersion','teamId','profileId','projects','executionMode','startsAt','endsAt','durationSeconds','objectiveFilters','planningOnly','proposalIds','decisionIds','operatorConstraints','agentSelection','allocation']);
const forbidden=Object.keys(input).filter((key)=>!allowed.has(key));
if(forbidden.length) diagnosticsError('workday_intent_derived_fields_forbidden','Workday preflight accepts high-level intent only.',forbidden.map((path)=>({code:'field_forbidden',path})));
if(input.teamId!==undefined&&text(input.teamId)!==teamId) diagnosticsError('workday_intent_team_mismatch','Workday intent team must match the route team.',[{code:'team_mismatch',path:'teamId'}]);
Expand All @@ -46,6 +46,7 @@ export function parsePublicWorkdayIntent(teamId:string,input:JsonRecord):Workday
const intent:WorkdayIntent={
schemaVersion:'treeseed.workday-intent/v1', teamId, profileId:text(input.profileId)||'default', projects,
startsAt,
...(input.executionMode !== undefined ? { executionMode: input.executionMode as WorkdayIntent['executionMode'] } : {}),
...(input.endsAt!==undefined?{endsAt:text(input.endsAt)}:{}),
...(input.durationSeconds!==undefined?{durationSeconds:Number(input.durationSeconds)}:{}),
...(Array.isArray(input.objectiveFilters)?{objectiveFilters:input.objectiveFilters.map(text).filter(Boolean)}:{}),
Expand All @@ -62,7 +63,8 @@ export function parsePublicWorkdayIntent(teamId:string,input:JsonRecord):Workday
const diagnostics=validateWorkdayIntent(intent);
if(projects!=='all'&&!projects.length) diagnostics.push({code:'projects_required',path:'projects',message:'Select at least one project or all.'});
if(diagnostics.length) diagnosticsError('workday_intent_invalid','Workday intent is invalid.',diagnostics);
if(intent.agentSelection!==undefined) intent.agentSelection=normalizeWorkdayAgentSelection(intent.agentSelection);
if(intent.agentSelection!==undefined) intent.agentSelection=Object.fromEntries(Object.entries(normalizeWorkdayAgentSelection(intent.agentSelection))
.filter(([,value])=>!Array.isArray(value)||value.length>0)) as WorkdayIntent['agentSelection'];
return intent;
}

Expand Down Expand Up @@ -98,7 +100,7 @@ export class WorkdayPreflightService {
}
const runInput:JsonRecord={
id:`workday-${id}`,capacityProviderId:providerId,status:'running',startedAt:startsAt,requestedById,
executionMode:'simulation',executionKind:'workday',triggerKind:'manual',
executionMode:intent.executionMode ?? 'simulation',executionKind:'workday',triggerKind:'manual',
environment:'local',scenarioId:`profile:${intent.profileId}`,
parameters:{ ...policy, ...intent.allocation, policyId:profile.id, policyRevision:profile.revision,
profileId:intent.profileId,projectSlugs:intent.projects==='all'?[]:intent.projects,
Expand Down Expand Up @@ -156,9 +158,9 @@ export class WorkdayPreflightService {
return {receipt,intent,runInput};
}

async preflight(teamId:string,intent:WorkdayIntent,requestedById:string|null):Promise<WorkdayPreflightReceipt> {
const stored=await this.compile(teamId,intent,requestedById,randomUUID());
await this.store.run(`INSERT INTO capacity_operation_receipts (id,team_id,operation,idempotency_key,request_digest,resource_type,resource_id,response_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)`,[
async preflight(teamId:string,intent:WorkdayIntent,requestedById:string|null,id=randomUUID()):Promise<WorkdayPreflightReceipt> {
const stored=await this.compile(teamId,intent,requestedById,id);
await this.store.run(`INSERT INTO capacity_operation_receipts (id,team_id,operation,idempotency_key,request_digest,resource_type,resource_id,response_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) ON CONFLICT (team_id,operation,idempotency_key) DO UPDATE SET request_digest=EXCLUDED.request_digest,response_json=EXCLUDED.response_json,updated_at=EXCLUDED.updated_at`,[
randomUUID(),teamId,'workday.preflight',stored.receipt.id,stored.receipt.intentDigest,'workday_preflight',stored.receipt.id,canonicalJson(stored),new Date().toISOString(),new Date().toISOString(),
]);
return stored.receipt;
Expand Down
Loading
Loading