Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .agentworkforce/agents/factory-feature-guardian/persona.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
"intent": "relay-orchestrator",
"tags": ["factory", "verification", "proactive", "health", "release-safety", "durability"],
"description": "Traverses the Factory feature catalog hourly to exercise the manifest/model/state chain with log-only healthy evidence, without claiming per-feature E2E verification; genuine run failures surface through deployment monitoring, while scoped Slack response events retain exact-revision incident handling.",
"skills": [
{
"id": "factory-feature-verification",
"source": ".agentworkforce/features/verify/procedures.md",
"description": "Verify Factory features against their named end-to-end procedures and supported safety boundaries."
}
],
"relay": {
"agentName": "factory-feature-guardian"
},
"cloud": true,
"harness": "opencode",
"model": "deepseek-v4-flash-free",
Expand Down
48 changes: 42 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@
"@agent-relay/integration-prompts": "^11.6.9",
"@agent-relay/sdk": "^11.6.9",
"@agentworkforce/delivery": "^4.1.23",
"@agentworkforce/persona-kit": "^4.1.43",
"@agentworkforce/review-kit": "^4.1.34",
"@agentworkforce/runtime": "^4.1.23",
"@relaycast/a2a": "^6.2.0",
"@relayfile/relay-helpers": "^0.4.6",
"@relayfile/sdk": "0.10.34",
"@relayflows/core": "^1.0.3",
Expand All @@ -118,7 +120,6 @@
"listr2": "9.0.5"
},
"devDependencies": {
"@agentworkforce/persona-kit": "^4.1.23",
"@types/node": "^22.10.2",
"@types/proper-lockfile": "^4.1.4",
"esbuild": "^0.28.1",
Expand Down
25 changes: 24 additions & 1 deletion src/fleet/internal-fleet-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { dirname, join } from 'node:path'
import type { BrokerEvent, ListAgent, SendMessageInput, SpawnPtyInput } from '@agent-relay/harness-driver'

import type { PreviewConfig } from '../config/schema'
import type { AgentMessage, AgentPidResolution, AgentUsage, Capability, FleetClient, FleetTrackedAgent, PreviewReference, PreviewStartInput, PreviewSweepInput, PreviewSweepResult, RosterEntry, SendInput, SpawnInput, SpawnResult } from '../ports/fleet'
import type { AgentMessage, AgentPidResolution, AgentUsage, Capability, FleetClient, FleetTrackedAgent, PreviewReference, PreviewStartInput, PreviewSweepInput, PreviewSweepResult, RosterEntry, SendInput, SpawnInput, SpawnResult, TeammateAgent, TeammateQuery } from '../ports/fleet'
import type { Logger } from '../ports/system'
import { normalizeLogger } from '../logging'
import { TailscalePreviewManager, type PreviewManager } from '../node/tailscale-preview'
import { resolveRelayWorkspaceKey } from './relay-workspace-key'
import { RelaycastTeammateDirectory, type TeammateDirectory } from './teammates'

const requireForResolve = createRequire(import.meta.url)

Expand Down Expand Up @@ -51,6 +52,11 @@ export interface InternalFleetClientOptions {
cwd?: string
connectionPath?: string
workspaceKey?: string
/** Card-aware directory seam; useful with a local broker plus mock directory. */
teammateDirectory?: TeammateDirectory
directoryBaseUrl?: string
directoryFetch?: typeof globalThis.fetch
directoryTimeoutMs?: number
/** Canonical cloud liveness lookup. Injected by tests; derived from workspaceKey in production. */
listCanonicalOnlineAgentNames?: () => Promise<readonly string[]>
/** Local process-liveness probe used to preserve workers that intentionally run without Relay MCP presence. */
Expand Down Expand Up @@ -111,6 +117,7 @@ export class InternalFleetClient implements FleetClient {
readonly #cwd?: string
readonly #connectionPath?: string
readonly #workspaceKey?: string
readonly #teammateDirectory?: TeammateDirectory
readonly #listCanonicalOnlineAgentNames?: () => Promise<readonly string[]>
readonly #isProcessAlive: (pid: number) => boolean
readonly #now: () => number
Expand Down Expand Up @@ -150,6 +157,15 @@ export class InternalFleetClient implements FleetClient {
this.#cwd = options.cwd
this.#connectionPath = options.connectionPath
this.#workspaceKey = options.workspaceKey
const directoryToken = resolveRelayWorkspaceKey({ workspaceKey: options.workspaceKey })
this.#teammateDirectory = options.teammateDirectory ?? (directoryToken
? new RelaycastTeammateDirectory({
baseUrl: options.directoryBaseUrl ?? process.env.RELAY_BASE_URL,
token: directoryToken,
fetch: options.directoryFetch,
timeoutMs: options.directoryTimeoutMs,
})
: undefined)
if (options.listCanonicalOnlineAgentNames) {
this.#listCanonicalOnlineAgentNames = options.listCanonicalOnlineAgentNames
} else if (options.workspaceKey) {
Expand Down Expand Up @@ -365,6 +381,13 @@ export class InternalFleetClient implements FleetClient {
}
}

async discoverTeammates(query: TeammateQuery): Promise<TeammateAgent[]> {
if (!this.#teammateDirectory) {
throw new Error('InternalFleetClient teammate discovery requires a directory or Relay workspace key')
}
return await this.#teammateDirectory.discover(query)
}

async createPreview(input: PreviewStartInput): Promise<PreviewReference> {
assertSelfNode(input.node)
if (!this.#previewManager) throw new Error('Tailscale preview provider is not configured')
Expand Down
34 changes: 33 additions & 1 deletion src/fleet/relay-fleet-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { AgentRelay } from '@agent-relay/sdk'

import { resolveRelayAgentToken, resolveRelayWorkspaceKey } from './relay-workspace-key'

import type { AgentLifecycleSignal, AgentMessage, AgentUsage, Capability, FleetClient, NodeCapability, PreviewReference, PreviewStartInput, PreviewSweepInput, PreviewSweepResult, RosterEntry, SendInput, SpawnInput, SpawnResult } from '../ports/fleet'
import type { AgentLifecycleSignal, AgentMessage, AgentUsage, Capability, FleetClient, NodeCapability, PreviewReference, PreviewStartInput, PreviewSweepInput, PreviewSweepResult, RosterEntry, SendInput, SpawnInput, SpawnResult, TeammateAgent, TeammateQuery } from '../ports/fleet'
import { RelaycastTeammateDirectory, type TeammateDirectory } from './teammates'
import type {
RelayActionInvocation,
RelayActionInvocationAck,
Expand Down Expand Up @@ -48,6 +49,10 @@ export interface RelayFleetClientOptions {
lifecycleActionName?: string
/** Engine base URL override. Absent means the SDK default (cast.agentrelay.com). */
baseUrl?: string
/** Card-aware directory seam. Defaults to Relaycast GET /v1/a2a/directory. */
teammateDirectory?: TeammateDirectory
directoryFetch?: typeof globalThis.fetch
directoryTimeoutMs?: number
/** Timeout for a spawn/release invocation to reach a terminal ack status. */
spawnAckTimeoutMs?: number
pollIntervalMs?: number
Expand Down Expand Up @@ -110,6 +115,7 @@ export class RelayFleetClient implements FleetClient {
// therefore createFleet({ backend: 'relay' })) never throws merely because no
// token is configured.
#messaging: RelayMessaging | undefined
#teammateDirectory: TeammateDirectory | undefined
#messagingReady: Promise<RelayMessaging> | undefined
#lifecycleActionReady: Promise<void> | undefined
#authenticatedAgentName: string
Expand All @@ -134,6 +140,7 @@ export class RelayFleetClient implements FleetClient {
this.#sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
this.#log = options.log ?? (() => {})
this.#messaging = options.messaging
this.#teammateDirectory = options.teammateDirectory
}

/** Agents spawned through this client that have not exited or been released. */
Expand Down Expand Up @@ -373,6 +380,11 @@ export class RelayFleetClient implements FleetClient {
}
}

async discoverTeammates(query: TeammateQuery): Promise<TeammateAgent[]> {
this.#teammateDirectory ??= this.#createTeammateDirectory()
return await this.#teammateDirectory.discover(query)
}

// `from`/`data` are not representable on the agent-scoped messaging surface:
// every send is authored by the factory's own agent identity.
async sendMessage(input: SendInput): Promise<void> {
Expand Down Expand Up @@ -495,6 +507,26 @@ export class RelayFleetClient implements FleetClient {
}
}

#createTeammateDirectory(): TeammateDirectory {
const env = this.#options.env
const token = resolveRelayWorkspaceKey({
workspaceKey: this.#options.workspaceKey,
...(env ? { env, activeWorkspaceKey: () => undefined } : {}),
}) ?? resolveRelayAgentToken({
agentToken: this.#options.agentToken,
...(env ? { env } : {}),
})
if (!token) {
throw new Error('RelayFleetClient teammate discovery requires a workspace key or agent token')
}
return new RelaycastTeammateDirectory({
baseUrl: this.#options.baseUrl,
token,
fetch: this.#options.directoryFetch,
timeoutMs: this.#options.directoryTimeoutMs,
})
}

#ensureMessaging(): Promise<RelayMessaging> {
if (this.#messaging) return Promise.resolve(this.#messaging)
this.#messagingReady ??= this.#bootstrapMessaging().catch((error) => {
Expand Down
Loading