refactor(session): route construction through Session.create
This commit is contained in:
@@ -67,7 +67,7 @@ async function harness(): Promise<Harness> {
|
||||
await ctx.plugin(CommandService)
|
||||
const compact = new StubCompactService(ctx)
|
||||
const plugin = await ctx.plugin(commandCompact)
|
||||
const session = new Session(SessionId('command-compact'))
|
||||
const session = Session.create(SessionId('command-compact'))
|
||||
const agent = {
|
||||
session,
|
||||
status: 'idle',
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('command-compact real Loader composition', () => {
|
||||
})
|
||||
await context.loader.await()
|
||||
|
||||
const session = new Session(SessionId('loader-command-compact'))
|
||||
const session = Session.create(SessionId('loader-command-compact'))
|
||||
const agent = {
|
||||
session,
|
||||
status: 'idle',
|
||||
|
||||
@@ -103,7 +103,7 @@ function promptInput(text: string): SummarizationInput {
|
||||
|
||||
/** Closed two-message turns followed by one open turn for durable compaction events. */
|
||||
function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session {
|
||||
const session = new Session(SessionId(`conversation-${turns}`))
|
||||
const session = Session.create(SessionId(`conversation-${turns}`))
|
||||
for (let turn = 1; turn <= turns; turn += 1) {
|
||||
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('user/message', createUserMessage({
|
||||
@@ -140,7 +140,7 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session {
|
||||
}
|
||||
|
||||
function toolConversation(): Session {
|
||||
const session = new Session(SessionId('tools'))
|
||||
const session = Session.create(SessionId('tools'))
|
||||
for (let turn = 1; turn <= 3; turn += 1) {
|
||||
const callId = CallId(`call-${turn}`)
|
||||
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
@@ -189,7 +189,7 @@ function toolConversation(): Session {
|
||||
|
||||
/** One closed routed tool step followed by an open turn for rewrite events. */
|
||||
function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Session {
|
||||
const session = new Session(SessionId(`oversized-tool-${chars}`))
|
||||
const session = Session.create(SessionId(`oversized-tool-${chars}`))
|
||||
const callId = CallId('oversized')
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
if (withCompactablePrompt) {
|
||||
@@ -485,7 +485,7 @@ describe('pressure measurement and retention', () => {
|
||||
|
||||
it('skips when no durable routed model exists instead of using AgentOptions fallback', async () => {
|
||||
const compact = service(compactConfig)
|
||||
const session = new Session(SessionId('headerless'))
|
||||
const session = Session.create(SessionId('headerless'))
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
await expect(compact.compactIfNeeded(agent(session, MODEL), 'pressure', SIGNAL))
|
||||
.resolves.toBeNull()
|
||||
@@ -567,7 +567,7 @@ describe('pressure measurement and retention', () => {
|
||||
|
||||
it('declines forced overflow when the whole surface is one indivisible tool pair', async () => {
|
||||
const compact = service(compactConfig)
|
||||
const session = new Session(SessionId('single-tool-pair'))
|
||||
const session = Session.create(SessionId('single-tool-pair'))
|
||||
const callId = CallId('single-call')
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
@@ -658,7 +658,7 @@ describe('pressure measurement and retention', () => {
|
||||
|
||||
it('declines when envelope pressure is high but the surface has no compactable range', async () => {
|
||||
const compact = service(compactConfig)
|
||||
const empty = new Session(SessionId('empty'))
|
||||
const empty = Session.create(SessionId('empty'))
|
||||
empty.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
empty.append('request/header', {
|
||||
header: { config: { provider: MODEL, model: MODEL }, system: 'x'.repeat(100_000) },
|
||||
@@ -733,7 +733,7 @@ describe('pressure measurement and retention', () => {
|
||||
|
||||
it('declines when rounding a cut would consume the only tool pair', () => {
|
||||
const ctx = createContext()
|
||||
const session = new Session(SessionId('one-tool-pair'))
|
||||
const session = Session.create(SessionId('one-tool-pair'))
|
||||
const callId = CallId('only')
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
@@ -874,7 +874,7 @@ describe('compaction region transaction', () => {
|
||||
expect(head.content[0]?.type === 'text' ? head.content[0].text : '').toContain('<compacted-summary>')
|
||||
expect(head.content.at(-1)).toEqual({ type: 'text', text: '</compacted-summary>' })
|
||||
|
||||
const replay = new Session(SessionId('replay'), [...session.events])
|
||||
const replay = Session.create(SessionId('replay'), [...session.events])
|
||||
expect(replay.deriveMessages()).toEqual(session.deriveMessages())
|
||||
})
|
||||
|
||||
@@ -956,7 +956,7 @@ describe('compaction region transaction', () => {
|
||||
|
||||
it('rejects a session with no turn boundary at all', async () => {
|
||||
const compact = service()
|
||||
const session = new Session(SessionId('turnless'))
|
||||
const session = Session.create(SessionId('turnless'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'orphan' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -1076,7 +1076,7 @@ describe('compaction region transaction', () => {
|
||||
|
||||
it('lets a model-independent custom summarizer compact without a conversation model', async () => {
|
||||
const compact = service()
|
||||
const session = new Session(SessionId('model-less-region'))
|
||||
const session = Session.create(SessionId('model-less-region'))
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'history '.repeat(100) }],
|
||||
@@ -1313,13 +1313,13 @@ describe('default one-shot summarizer', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
void new TokenMeterService(ctx)
|
||||
const compact = new ExposedCompactService(ctx, { auto: false })
|
||||
await expect(compact.runSummarize(promptInput('history'), agent(new Session(SessionId('model-less')))))
|
||||
await expect(compact.runSummarize(promptInput('history'), agent(Session.create(SessionId('model-less')))))
|
||||
.rejects.toThrow(/no provider\/model available for summarization/)
|
||||
})
|
||||
|
||||
it('uses a complete AgentOptions target when no durable route exists', async () => {
|
||||
const { adapter, compact } = await summarizerHarness([{ type: 'text', text: 'summary' }])
|
||||
const session = new Session(SessionId('headerless-summary'))
|
||||
const session = Session.create(SessionId('headerless-summary'))
|
||||
|
||||
await expect(compact.runSummarize(promptInput('history'), agent(session, MODEL))).resolves.toMatchObject({
|
||||
provider: MODEL,
|
||||
@@ -1335,7 +1335,7 @@ describe('default one-shot summarizer', () => {
|
||||
])('rejects incomplete AgentOptions target %#', async (options) => {
|
||||
const { compact } = await summarizerHarness([{ type: 'text', text: 'unused' }])
|
||||
const owner = {
|
||||
session: new Session(SessionId(`incomplete-${String(options.model)}`)),
|
||||
session: Session.create(SessionId(`incomplete-${String(options.model)}`)),
|
||||
options,
|
||||
} as Agent
|
||||
await expect(compact.runSummarize(promptInput('history'), owner))
|
||||
@@ -1698,7 +1698,7 @@ describe('automatic listener and loader composition', () => {
|
||||
it('delegates canonical overflow when no durable routed target exists', async () => {
|
||||
const ctx = createContext()
|
||||
void new TestCompactService(ctx)
|
||||
const session = new Session(SessionId('headerless-overflow'))
|
||||
const session = Session.create(SessionId('headerless-overflow'))
|
||||
session.append('turn/start', {
|
||||
turn: 1,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
|
||||
@@ -185,7 +185,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
}
|
||||
|
||||
function overflowHistorySeed(): SessionEvent[] {
|
||||
const session = new Session(SessionId('overflow-history-seed'))
|
||||
const session = Session.create(SessionId('overflow-history-seed'))
|
||||
for (let turn = 1; turn <= 2; turn += 1) {
|
||||
const sentinel = turn === 1 ? 'OLD HISTORY SENTINEL' : 'RECENT HISTORY'
|
||||
session.append('turn/start', {
|
||||
|
||||
@@ -166,7 +166,7 @@ function deferred(): { promise: Promise<undefined>; resolve: () => void } {
|
||||
|
||||
/** A closed-tail session with compactable exchanges and no live agent. */
|
||||
function closedConversation(turns = 2, lastTurnNumber = turns): Session {
|
||||
const session = new Session(SessionId(`closed-${turns}-${lastTurnNumber}`))
|
||||
const session = Session.create(SessionId(`closed-${turns}-${lastTurnNumber}`))
|
||||
for (let index = 1; index <= turns; index += 1) {
|
||||
const turn = index === turns ? lastTurnNumber : index
|
||||
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
@@ -368,7 +368,7 @@ describe('compactNow through the real loop', () => {
|
||||
describe('compactNow transaction and failure classification', () => {
|
||||
it('returns null without writing a bracket for history that cannot be compacted', async () => {
|
||||
const { compact } = detachedService()
|
||||
const session = new Session(SessionId('empty'))
|
||||
const session = Session.create(SessionId('empty'))
|
||||
let released = 0
|
||||
const agent = fakeAgent(session, () => () => { released += 1 })
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const { compact } = detachedService()
|
||||
const original = closedConversation(2)
|
||||
original.append('compact/start', { turn: null })
|
||||
const reloaded = new Session(SessionId('stale-orphan'), [...original.events])
|
||||
const reloaded = Session.create(SessionId('stale-orphan'), [...original.events])
|
||||
const boundary = reloaded.events.findLast(event => event.type === 'session/end-seed')
|
||||
const orphan = reloaded.events.find(event => event.type === 'compact/start')
|
||||
const agent = fakeAgent(reloaded, () => () => undefined)
|
||||
@@ -426,7 +426,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
original.append('compact/start', { turn: null })
|
||||
original.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
original.append('turn/end', { turn: 3, reason: { kind: 'interrupted' } })
|
||||
const reloaded = new Session(SessionId('reloaded-orphan'), [...original.events])
|
||||
const reloaded = Session.create(SessionId('reloaded-orphan'), [...original.events])
|
||||
const agent = fakeAgent(reloaded, () => () => undefined)
|
||||
|
||||
await expect(compact.compactNow(agent, SIGNAL)).resolves.not.toBeNull()
|
||||
@@ -638,7 +638,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
|
||||
it('compacts a session with no durable turn boundary without creating one', async () => {
|
||||
const { compact } = detachedService()
|
||||
const session = new Session(SessionId('turnless'))
|
||||
const session = Session.create(SessionId('turnless'))
|
||||
for (const text of [PROMPT, 'recent tail']) {
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text }],
|
||||
@@ -671,7 +671,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
it('lets a pre-aborted signal win before reservation, measurement, or summarization', async () => {
|
||||
const cases = [
|
||||
{ name: 'busy', session: closedConversation(2), release: undefined },
|
||||
{ name: 'empty', session: new Session(SessionId('pre-aborted-empty')), release: () => undefined },
|
||||
{ name: 'empty', session: Session.create(SessionId('pre-aborted-empty')), release: () => undefined },
|
||||
{ name: 'compactable', session: closedConversation(2, 9), release: () => undefined },
|
||||
] as const
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('ToolResultPruneService content transform', () => {
|
||||
|
||||
describe('ToolResultPruneService session transaction', () => {
|
||||
it('prunes a stable snapshot, preserves all data, and records provenance', () => {
|
||||
const session = new Session(SessionId('preserve'))
|
||||
const session = Session.create(SessionId('preserve'))
|
||||
const originalSeq = appendToolStep(session, 1, 'one', [{
|
||||
type: 'text',
|
||||
text: 'x'.repeat(100),
|
||||
@@ -208,7 +208,7 @@ describe('ToolResultPruneService session transaction', () => {
|
||||
})
|
||||
|
||||
it('prunes multiple results, skips short ones, and converges in one pass', () => {
|
||||
const session = new Session(SessionId('multiple'))
|
||||
const session = Session.create(SessionId('multiple'))
|
||||
appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }])
|
||||
appendToolStep(session, 2, 'b', [{ type: 'text', text: 'short' }])
|
||||
appendToolStep(session, 3, 'c', [{ type: 'text', text: 'C'.repeat(80) }])
|
||||
@@ -227,14 +227,14 @@ describe('ToolResultPruneService session transaction', () => {
|
||||
})
|
||||
|
||||
it('replays to the identical pruned model messages', () => {
|
||||
const session = new Session(SessionId('replay'))
|
||||
const session = Session.create(SessionId('replay'))
|
||||
appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }])
|
||||
session.append('turn/start', {
|
||||
turn: 2,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
service().pruneSession(session)
|
||||
const replay = new Session(session.id, [...session.events])
|
||||
const replay = Session.create(session.id, [...session.events])
|
||||
expect(replay.deriveMessages()).toEqual(session.deriveMessages())
|
||||
expect(replay.surface.replaceGeneration).toBe(session.surface.replaceGeneration)
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('CompactService seam', () => {
|
||||
it('exposes the abstract contract methods', async () => {
|
||||
const ctx = new Context()
|
||||
const svc = new StubCompactService(ctx)
|
||||
const session = new Session(SessionId('s'))
|
||||
const session = Session.create(SessionId('s'))
|
||||
expect(await svc.compactIfNeeded(stubAgent(session), 'pressure', new AbortController().signal)).toBeNull()
|
||||
const signal = new AbortController().signal
|
||||
expect(await svc.compactNow({
|
||||
@@ -118,7 +118,7 @@ describe('CompactService seam', () => {
|
||||
it('compact/* events merge into SessionEventMap and are log-only', async () => {
|
||||
const ctx = new Context()
|
||||
const svc = new StubCompactService(ctx)
|
||||
const session = new Session(SessionId('s'))
|
||||
const session = Session.create(SessionId('s'))
|
||||
const original = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'original' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -149,7 +149,7 @@ describe('CompactService seam', () => {
|
||||
it('threads the cancellation signal through to the backend', async () => {
|
||||
const ctx = new Context()
|
||||
const svc = new StubCompactService(ctx)
|
||||
const session = new Session(SessionId('s'))
|
||||
const session = Session.create(SessionId('s'))
|
||||
const controller = new AbortController()
|
||||
const original = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'original' }],
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('compaction invariants', () => {
|
||||
it('clears an inherited open compaction trace at end-seed during replay', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const source = new Session(SessionId('stale-compaction-source'))
|
||||
const source = Session.create(SessionId('stale-compaction-source'))
|
||||
source.append('compact/start', { turn: null })
|
||||
const replayed = ctx.sessions.create(SessionId('stale-compaction-replay'), {
|
||||
seed: source.events,
|
||||
@@ -76,7 +76,7 @@ describe('compaction invariants', () => {
|
||||
it('allows repair turn boundaries after end-seed clears a seeded numbered orphan', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const source = new Session(SessionId('stale-numbered-compaction-source'))
|
||||
const source = Session.create(SessionId('stale-numbered-compaction-source'))
|
||||
startTurn(source)
|
||||
source.append('compact/start', { turn: 1 })
|
||||
const replayed = ctx.sessions.create(SessionId('stale-numbered-compaction-replay'), {
|
||||
@@ -97,7 +97,7 @@ describe('compaction invariants', () => {
|
||||
it('accepts inherited repair boundaries before the end-seed that clears a standalone orphan', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const source = new Session(SessionId('stale-repaired-compaction-source'))
|
||||
const source = Session.create(SessionId('stale-repaired-compaction-source'))
|
||||
source.append('compact/start', { turn: null })
|
||||
startTurn(source)
|
||||
source.append('turn/end', { turn: 1, reason: { kind: 'interrupted' } })
|
||||
@@ -123,7 +123,7 @@ describe('compaction invariants', () => {
|
||||
it('rejects a closed standalone bracket that contains a turn before end-seed', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const source = new Session(SessionId('closed-nested-compaction-source'))
|
||||
const source = Session.create(SessionId('closed-nested-compaction-source'))
|
||||
source.append('compact/start', { turn: null })
|
||||
startTurn(source)
|
||||
source.append('turn/end', { turn: 1, reason: { kind: 'interrupted' } })
|
||||
@@ -152,7 +152,7 @@ describe('compaction invariants', () => {
|
||||
|
||||
it('adopts a bare session and ignores unrelated committed events', async () => {
|
||||
const ctx = await setup()
|
||||
const session = new Session(SessionId('bare-compaction-session'))
|
||||
const session = Session.create(SessionId('bare-compaction-session'))
|
||||
expect(() => {
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/start', seq: 0, time: 0,
|
||||
|
||||
@@ -25,7 +25,7 @@ function after(session: Session, type: SessionEvent['type'], nth = 0): boolean {
|
||||
}
|
||||
|
||||
function closedToolStep(): Session {
|
||||
const session = new Session(SessionId('closed-tool-step'))
|
||||
const session = Session.create(SessionId('closed-tool-step'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'go' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -64,7 +64,7 @@ describe('tool-pairing boundaries', () => {
|
||||
expect(before(closed, 'tool/result')).toBe(false)
|
||||
expect(after(closed, 'tool/result')).toBe(true)
|
||||
|
||||
const open = new Session(SessionId('open-tool-step'))
|
||||
const open = Session.create(SessionId('open-tool-step'))
|
||||
open.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
@@ -81,7 +81,7 @@ describe('tool-pairing boundaries', () => {
|
||||
})
|
||||
|
||||
it('requires every result from a multiple-call assistant message', () => {
|
||||
const session = new Session(SessionId('multiple-calls'))
|
||||
const session = Session.create(SessionId('multiple-calls'))
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
@@ -119,7 +119,7 @@ describe('tool-pairing boundaries', () => {
|
||||
})
|
||||
|
||||
it('keeps neutral nodes inside an open pair unbalanced and free nodes balanced', () => {
|
||||
const midStep = new Session(SessionId('neutral-mid-step'))
|
||||
const midStep = Session.create(SessionId('neutral-mid-step'))
|
||||
midStep.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
@@ -147,7 +147,7 @@ describe('tool-pairing boundaries', () => {
|
||||
expect(before(midStep, 'user/message')).toBe(false)
|
||||
expect(after(midStep, 'user/message')).toBe(false)
|
||||
|
||||
const free = new Session(SessionId('neutral-free'))
|
||||
const free = Session.create(SessionId('neutral-free'))
|
||||
free.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'idle injection' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -187,7 +187,7 @@ describe('tool-pairing surface identity', () => {
|
||||
})
|
||||
|
||||
it('rejects missing seqs before and after, including an empty surface', () => {
|
||||
const session = new Session(SessionId('missing-membership'))
|
||||
const session = Session.create(SessionId('missing-membership'))
|
||||
const missing = 999
|
||||
expect(() => toolPairingBalancedBefore(session, missing)).toThrow(/surface seq 999 not found/)
|
||||
expect(() => toolPairingBalancedAfter(session, missing)).toThrow(/surface seq 999 not found/)
|
||||
@@ -367,7 +367,7 @@ describe('tool-pairing cache refresh', () => {
|
||||
|
||||
describe('tool-pairing corrupt surfaces', () => {
|
||||
it('throws for an orphan result during a rebuild', () => {
|
||||
const session = new Session(SessionId('orphan-rebuild'))
|
||||
const session = Session.create(SessionId('orphan-rebuild'))
|
||||
session.append('tool/result', {
|
||||
turn: 1, step: 1,
|
||||
message: createToolResultMessage({
|
||||
@@ -380,7 +380,7 @@ describe('tool-pairing corrupt surfaces', () => {
|
||||
})
|
||||
|
||||
it('retries an orphan result in an appended tail without committing partial cache state', () => {
|
||||
const session = new Session(SessionId('orphan-tail'))
|
||||
const session = Session.create(SessionId('orphan-tail'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'safe head' }], source: { kind: 'user' },
|
||||
}), SURFACE)
|
||||
|
||||
Reference in New Issue
Block a user