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 build/local-uar-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"schema": 1,
"platform": "darwin-arm64",
"repository": "Prometheus-AGS/universal-agent-runtime",
"revision": "ae4db11869efd125dce7f9a2a488d876af0b7b98",
"revision": "d3d768636b46bd7b7cea29fba4987aa1754f8352",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Design

The existing authenticated sidecar adapter resolves a Boss workspace before any UAR call. Its fixed operations post revisioned commands to UAR and parse scoped returned instances/messages. Renderer IPC accepts typed IDs, content and expected revisions, never an arbitrary endpoint. UAR rechecks owner, workspace and recipient at the durable mutation boundary; the renderer is not an authority source.

The Teams panel retains UAR as the board source of truth. After each successful mutation it replaces the affected team from the server response; on conflict it refreshes and presents a retry action. Message status comes only from UAR receipt fields. The page labels planning and trigger intent accurately, without implying that a model is already running. At completion, build the actual Mac ARM64 application and exercise assignments, fencing, inbox persistence and page feedback against its packaged sidecar.
15 changes: 15 additions & 0 deletions openspec/changes/uar-c09-task-ownership-mailbox-admin/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# C09.2: Team ownership and mailbox administration

## Why

The installed Teams page can plan tasks, but cannot assign a member or inspect communication. UAR now needs one trusted, workspace-scoped administration path for claims and inbox receipts.

## What changes

- Extend the fixed main-process UAR adapter and typed IPC with claim, reassign, reviewer and inbox operations.
- Display task state, owner, ownership epoch and reviewer; send addressed `queue-only` or `trigger-turn` messages and show accepted/delivered/processed status.
- Translate all controls, results and errors in every shipped locale. Package the matching UAR sidecar.

## Boundary

The Boss never owns task state or accepts a renderer-selected URL. A trigger-turn action records intent only until C09.3 runtime admission. This increment has no Start Agent control.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## ADDED Requirements

### Requirement: The Boss administers scoped team assignments
The Boss SHALL expose fixed typed operations for a workspace's team task claim, reassignment and reviewer selection. It SHALL display the UAR-returned status, owner and fencing epoch and SHALL not infer that assignment starts execution.

#### Scenario: Reassign a task in the installed app
- **WHEN** an operator reassigns a ready task through Teams settings
- **THEN** the page shows the returned owner and newer epoch, and a stale revision is reported as a conflict

### Requirement: The Boss shows durable team communication truthfully
The Boss SHALL send an addressed team message through its trusted UAR adapter and show accepted, delivered and processed as separate states. It SHALL not represent `trigger-turn` intent as a running agent until UAR reports an admitted turn.

#### Scenario: Reopen after restart
- **WHEN** the installed app restarts after sending a message
- **THEN** the Teams page reads the same UAR message and displays its actual persisted receipt stage
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Tasks

- [x] 1. Add typed task and mailbox projections, fixed scoped adapter methods and IPC contracts.
- [x] 2. Add Teams task ownership, reviewer and mailbox controls with explicit state feedback and all locale strings.
- [x] 3. Package the exact matching UAR source and complete the installed Mac ARM64 build/function boundary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Leave the installed boundary unchecked until it runs

This marks the packaged Mac ARM64 build and installed functional boundary complete, but the same commit states that the boundary is still being finalized, merge/publication must wait, and no build or test was launched. Keeping the task checked records false release-readiness evidence; leave it open until the packaged assignment, fencing, and mailbox flow has actually been exercised.

AGENTS.md reference: AGENTS.md:L57-L58

Useful? React with 👍 / 👎.

4 changes: 2 additions & 2 deletions src/main/ai/runtime/uar/UarStarterAdministrationAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ export async function setupUarStarterTeam(workspaceId: string): Promise<UarTeamB
runtimeInstanceId: capabilities.instance.id,
storageBackend: endpoint.storage.profile.backend,
packageIdentity: starter.identity,
bindingId: `urn:boss:starter:team-binding:${resolved}`,
effectiveLimits: { concurrentTurns: 1, maxMembers: 2, maxDepth: 0, maxPendingTasks: 8 }
bindingId: `urn:boss:starter:team-binding:v2:${resolved}`,
effectiveLimits: { concurrentTurns: 1, maxMembers: 3, maxDepth: 0, maxPendingTasks: 8 }
})
const existingBindings = z.array(rawBinding).parse(await scopedRequest(resolved, bindingPath, state.generation))
if (existingBindings.some((candidate) => candidate.workspaceId !== resolved)) {
Expand Down
146 changes: 140 additions & 6 deletions src/main/ai/runtime/uar/UarTeamsAdministrationAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import * as z from 'zod'

import type { UarAddTeamTaskInput, UarCreateTeamInput, UarTeamInstance, UarTeamsSnapshot } from '@shared/types/uarTeams'
import type {
UarAddTeamTaskInput,
UarCreateTeamInput,
UarTeamInstance,
UarTeamMailboxMessage,
UarTeamMailboxPage,
UarTeamMailboxSendInput,
UarTeamTaskCommandInput,
UarTeamTaskStateInput,
UarTeamsSnapshot
} from '@shared/types/uarTeams'

import { capabilityState, rawBinding, scopedRequest, workspace } from './UarDurableAdministrationAdapter'

Expand Down Expand Up @@ -28,8 +38,26 @@ const task = z.object({
outputContract: z.unknown(),
output: z.null(),
dependsOn: z.array(z.string()),
status: z.literal('queued'),
status: z.enum(['queued', 'ready', 'succeeded', 'failed', 'cancelled']),
revision: z.number().int().nonnegative(),
assigneeMemberId: z.string().nullable().default(null),
ownershipEpoch: z.number().int().nonnegative().default(0),
assignmentAuthority: z
.object({
bindingId: z.string(),
bindingRevision: z.number().int().nonnegative(),
workspaceId: z.string(),
role: z.string(),
memberId: z.string(),
ownershipEpoch: z.number().int().nonnegative(),
canExecute: z.literal(false),
canUseTools: z.literal(false)
})
.nullable()
.default(null),
reviewerMemberId: z.string().nullable().default(null),
reviewerEpoch: z.number().int().nonnegative().default(0),
stateReason: z.string().nullable().default(null),
createdAt: z.string(),
updatedAt: z.string()
})
Expand Down Expand Up @@ -57,6 +85,24 @@ const instance = z.object({
createdAt: z.string(),
updatedAt: z.string()
})
const mailboxMessage = z.object({
messageId: z.string(),
ownerId: z.string(),
workspaceId: z.string(),
teamId: z.string(),
senderOwnerId: z.string(),
recipientMemberId: z.string(),
recipientMemberRevision: z.number().int().nonnegative(),
taskId: z.string().nullish(),
taskEpoch: z.number().int().nonnegative().nullish(),
mode: z.enum(['queue-only', 'trigger-turn']),
content: z.string(),
status: z.enum(['accepted', 'delivered', 'processed']),
acceptedAt: z.string(),
deliveredAt: z.string().nullish(),
processedAt: z.string().nullish(),
processedTurnId: z.string().nullish()
})

function scopedTeam(value: unknown, workspaceId: string): UarTeamInstance {
const team = instance.parse(value)
Expand All @@ -70,14 +116,20 @@ async function planningState(workspaceId: string) {
const capabilities = z
.object({
collaboration: z.object({
activation: z.object({ teamPlanning: z.boolean().default(false) })
activation: z.object({
teamPlanning: z.boolean().default(false),
taskOwnership: z.boolean().default(false),
teamMailbox: z.boolean().default(false)
})
})
})
.parse(await scopedRequest(resolved, '/api/v1/collaboration/capabilities', state.generation))
return {
workspaceId: resolved,
generation: state.generation,
planning: capabilities.collaboration.activation.teamPlanning
planning: capabilities.collaboration.activation.teamPlanning,
ownership: capabilities.collaboration.activation.taskOwnership,
mailbox: capabilities.collaboration.activation.teamMailbox
}
}

Expand All @@ -88,7 +140,7 @@ export async function readUarTeams(workspaceId: string): Promise<UarTeamsSnapsho
schemaVersion: 1,
workspaceId: state.workspaceId,
generation: state.generation,
capabilities: { planning: false },
capabilities: { planning: false, ownership: false, mailbox: false },
unavailableReason: 'team_planning_unsupported',
definitions: [],
bindings: [],
Expand All @@ -108,7 +160,7 @@ export async function readUarTeams(workspaceId: string): Promise<UarTeamsSnapsho
schemaVersion: 1,
workspaceId: state.workspaceId,
generation: state.generation,
capabilities: { planning: true },
capabilities: { planning: true, ownership: state.ownership, mailbox: state.mailbox },
definitions: z.array(definition).parse(definitions),
bindings: scopedBindings,
instances: z
Expand Down Expand Up @@ -152,3 +204,85 @@ export async function addUarTeamTask(input: UarAddTeamTaskInput): Promise<UarTea
state.workspaceId
)
}

async function mutateTeamTask(
input: Omit<UarTeamTaskCommandInput, 'memberId'>,
action: 'claim' | 'reassign' | 'reviewer' | 'state',
payload: object
): Promise<UarTeamInstance> {
const state = await planningState(input.workspaceId)
if (!state.ownership) throw new Error('UAR team task ownership is unavailable')
const target = `${basePath}/${encodeURIComponent(input.teamInstanceId)}`
scopedTeam(await scopedRequest(state.workspaceId, target, state.generation), state.workspaceId)
return scopedTeam(
await scopedRequest(
state.workspaceId,
`${target}/tasks/${encodeURIComponent(input.taskId)}/${action}`,
state.generation,
'POST',
{
commandId: input.commandId,
expectedTeamRevision: input.expectedTeamRevision,
expectedTaskRevision: input.expectedTaskRevision,
...payload
}
),
state.workspaceId
)
}

export async function claimUarTeamTask(input: UarTeamTaskCommandInput): Promise<UarTeamInstance> {
return mutateTeamTask(input, 'claim', { memberId: input.memberId })
}

export async function reassignUarTeamTask(input: UarTeamTaskCommandInput): Promise<UarTeamInstance> {
return mutateTeamTask(input, 'reassign', { memberId: input.memberId })
}

export async function assignUarTeamReviewer(input: UarTeamTaskCommandInput): Promise<UarTeamInstance> {
return mutateTeamTask(input, 'reviewer', { memberId: input.memberId })
}

export async function updateUarTeamTaskState(input: UarTeamTaskStateInput): Promise<UarTeamInstance> {
return mutateTeamTask(input, 'state', { status: input.status, reason: input.reason })
}

function scopedMailboxMessage(value: unknown, workspaceId: string, teamId: string): UarTeamMailboxMessage {
const message = mailboxMessage.parse(value)
if (message.workspaceId !== workspaceId || message.teamId !== teamId) {
throw new Error('UAR team mailbox scope mismatch')
}
return message
}

export async function readUarTeamMailbox(input: {
workspaceId: string
teamInstanceId: string
}): Promise<UarTeamMailboxPage> {
const state = await planningState(input.workspaceId)
if (!state.mailbox) throw new Error('UAR team mailbox is unavailable')
const target = `${basePath}/${encodeURIComponent(input.teamInstanceId)}`
scopedTeam(await scopedRequest(state.workspaceId, target, state.generation), state.workspaceId)
const page = z.object({ messages: z.array(z.unknown()) }).parse(
await scopedRequest(state.workspaceId, `${target}/messages`, state.generation)
)
return { messages: page.messages.map((value) => scopedMailboxMessage(value, state.workspaceId, input.teamInstanceId)) }
}

export async function sendUarTeamMailboxMessage(input: UarTeamMailboxSendInput): Promise<UarTeamMailboxMessage> {
const state = await planningState(input.workspaceId)
if (!state.mailbox) throw new Error('UAR team mailbox is unavailable')
const target = `${basePath}/${encodeURIComponent(input.teamInstanceId)}`
scopedTeam(await scopedRequest(state.workspaceId, target, state.generation), state.workspaceId)
return scopedMailboxMessage(
await scopedRequest(state.workspaceId, `${target}/messages`, state.generation, 'POST', {
commandId: input.commandId,
messageId: input.commandId,
recipientMemberId: input.recipientMemberId,
mode: input.mode,
content: input.content
}),
state.workspaceId,
input.teamInstanceId
)
}
12 changes: 11 additions & 1 deletion src/main/ai/runtime/uar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ export {
createUarDurableObserver,
readUarDurableWorkspace
} from './UarDurableAdministrationAdapter'
export { addUarTeamTask, createUarTeam, readUarTeams } from './UarTeamsAdministrationAdapter'
export {
addUarTeamTask,
assignUarTeamReviewer,
claimUarTeamTask,
createUarTeam,
readUarTeamMailbox,
readUarTeams,
reassignUarTeamTask,
sendUarTeamMailboxMessage,
updateUarTeamTaskState
} from './UarTeamsAdministrationAdapter'
export { setupUarStarterAgent, setupUarStarterTeam } from './UarStarterAdministrationAdapter'
export {
deleteUarProvider,
Expand Down
11 changes: 6 additions & 5 deletions src/main/ai/runtime/uar/uarStarterDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export function starterPackage() {

/** A separate immutable planning package; installing it never changes the existing starter agent binding. */
export function starterTeamPackage() {
const teamVersion = '1.0.1'
const starter = starterPackage()
const agentSource = starter.files[agentPath]
const agent = JSON.parse(agentSource) as { id: string; version: string; contentDigest: string }
Expand Down Expand Up @@ -134,28 +135,28 @@ export function starterTeamPackage() {
profile,
kind: 'TeamDefinition',
id: teamId,
version,
version: teamVersion,
provenance: { source: 'The Boss built-in starter team', authors: ['The Boss'] },
requiredCapabilities: [],
extensions: {},
title: 'Starter planning team',
purpose: 'Plan a workspace task with a coordinator and a worker.',
purpose: 'Plan a workspace task with a coordinator and up to two workers.',
members: [
{ role: 'coordinator', kind: 'agent', definition: agentRef, min: 1, max: 1, responsibility: 'Define the plan.' },
{
role: 'worker',
kind: 'agent',
definition: agentRef,
min: 1,
max: 1,
max: 2,
responsibility: 'Prepare the assigned work.'
}
],
coordinatorRole: 'coordinator',
communication: [],
taskAcceptance: { mode: 'operator', allowedWorkflows: [workflowRef] },
routing: { eligibilityFirst: true, strategy: 'operator-role-capacity-cost-stable-id', explain: true },
limits: { concurrentTurns: 1, maxMembers: 2, maxDepth: 0, maxPendingTasks: 8 },
limits: { concurrentTurns: 1, maxMembers: 3, maxDepth: 0, maxPendingTasks: 8 },
budget: { maxTokens: 10000, maxCostMicrounits: 1000000, currency: 'USD', maxElapsedSeconds: 300 },
input: { type: 'object' },
output: { type: 'object' }
Expand All @@ -166,7 +167,7 @@ export function starterTeamPackage() {
profile,
kind: 'PackageManifest',
id: 'urn:boss:starter:team-package',
version,
version: teamVersion,
provenance: { source: 'The Boss built-in starter team', authors: ['The Boss'] },
requiredCapabilities: ['collaboration_definition_packages_v2'],
extensions: {},
Expand Down
14 changes: 13 additions & 1 deletion src/main/ipc/handlers/prometheus.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { application } from '@application'
import {
addUarTeamTask,
assignUarTeamReviewer,
acknowledgeUarDurableGap,
actOnUarDurableInstance,
actOnUarDurableObserver,
compileUarAgent,
cancelUarRun,
claimUarTeamTask,
createUarDurableInstance,
createUarDurableObserver,
createUarTeam,
Expand All @@ -23,6 +25,7 @@ import {
readUarAdministrationSnapshot,
readUarDurableWorkspace,
readUarTeams,
readUarTeamMailbox,
readUarOperations,
readUarRunDetail,
readUarCatalog,
Expand All @@ -31,6 +34,7 @@ import {
readUarPresentations,
readUarSettings,
refreshUarSkills,
reassignUarTeamTask,
saveUarA2uiComponent,
saveUarAgent,
saveUarAgentSkills,
Expand All @@ -43,11 +47,13 @@ import {
setupUarStarterAgent,
setupUarStarterTeam,
searchUarKnowledge,
sendUarTeamMailboxMessage,
uploadUarKnowledgeDocument,
setDefaultUarProvider,
testUarProvider,
toggleUarSkill,
updateUarSettings
updateUarSettings,
updateUarTeamTaskState
} from '@main/ai/runtime/uar'
import { StaleIntegrationRevisionError } from '@main/services/prometheus/integrationErrors'
import {
Expand Down Expand Up @@ -149,6 +155,12 @@ const prometheusHandlerImplementations: IpcHandlersFor<typeof prometheusRequestS
'prometheus.uar.teams.setup_starter': async ({ workspaceId }) => setupUarStarterTeam(workspaceId),
'prometheus.uar.teams.create': async (input) => createUarTeam(input),
'prometheus.uar.teams.add_task': async (input) => addUarTeamTask(input),
'prometheus.uar.teams.claim_task': async (input) => claimUarTeamTask(input),
'prometheus.uar.teams.reassign_task': async (input) => reassignUarTeamTask(input),
'prometheus.uar.teams.assign_reviewer': async (input) => assignUarTeamReviewer(input),
'prometheus.uar.teams.update_task_state': async (input) => updateUarTeamTaskState(input),
'prometheus.uar.teams.mailbox_list': async (input) => readUarTeamMailbox(input),
'prometheus.uar.teams.mailbox_send': async (input) => sendUarTeamMailboxMessage(input),
'prometheus.uar.durable.setup_starter': async ({ workspaceId }) => setupUarStarterAgent(workspaceId),
'prometheus.uar.durable.create_instance': async (input) => createUarDurableInstance(input),
'prometheus.uar.durable.instance_action': async (input) => actOnUarDurableInstance(input),
Expand Down
Loading
Loading