Files
deepseek-harness/packages/experimental/agent-team/src/journal.ts
T

74 lines
2.8 KiB
TypeScript

/** Serialized Team transactions over the exact live Lead Session log. */
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Context } from '@deepseek-ai/cordis'
import type { SessionEventMap, SessionId } from '@deepseek-ai/dsh-session'
import type { TeamEventType, TeamState } from './projection.ts'
type AppendTeamEvent = <T extends TeamEventType>(type: T, data: SessionEventMap[T]) => void
type MutableTeamEventType = 'team/member' | 'team/task' | 'team/message/queued' | 'team/message/delivered'
/** Owns per-Lead transaction order and committed Team event publication. */
export class TeamJournal {
private readonly tails = new Map<SessionId, Promise<void>>()
/**
* @param ctx - Team service context with the injected Session service.
* @param onCommit - synchronous notification after the Team event flush succeeds.
*/
constructor(
private readonly ctx: Context,
private readonly onCommit: (root: Agent) => void,
) {}
/**
* Read authoritative Team state for one exact live Lead.
* @param root - exact live Team Lead.
* @returns current projected state selected by the Lead Team id.
*/
state(root: Agent): TeamState {
const projection = this.ctx.sessionProjections.stateOf(root.session, 'agentTeam')
if (projection === undefined) throw new Error('Agent Teams projection is not registered')
if (projection.failure !== undefined) throw new Error(projection.failure)
return projection
}
/**
* Serialize one Lead's asynchronous mutation operation.
* @param rootId - Lead Session identity selecting the transaction queue.
* @param operation - complete read-check-append operation.
* @returns the operation result.
*/
async transact<T>(rootId: SessionId, operation: () => Promise<T>): Promise<T> {
const prior = this.tails.get(rootId) ?? Promise.resolve()
const run = prior.then(operation, operation)
const tail = run.then(() => undefined, () => undefined)
this.tails.set(rootId, tail)
try {
return await run
} finally {
if (this.tails.get(rootId) === tail) this.tails.delete(rootId)
}
}
/**
* Append and checkpoint one root-owned Team event before publication.
* @param root - exact live Lead whose Session owns the event.
* @param type - Team event discriminant.
* @param data - payload correlated with the event type.
*/
async appendAndFlush<T extends MutableTeamEventType>(
root: Agent,
type: T,
data: SessionEventMap[T],
): Promise<void> {
// Team events never enter the conversation surface. This narrower local
// capability removes Session.append's conditional surface argument while
// preserving the event-key/payload correlation.
const append = root.session.append.bind(root.session) as unknown as AppendTeamEvent
append(type, data)
await this.ctx.sessions.flush(root.session)
this.onCommit(root)
}
}