Merge origin/master into feature/shared-cli-config-foundation
This commit is contained in:
@@ -221,7 +221,7 @@ export class ReactLoopAgent implements Agent {
|
||||
if (this.abort !== undefined || !this.queued.some(item => item.wakeup)) return
|
||||
// The some() guard above proves the queue is non-empty; the non-null
|
||||
// assertion expresses that invariant.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const { message } = this.queued.shift()!
|
||||
const inheritedOutboxLength = this.outbox.length
|
||||
|
||||
@@ -368,7 +368,7 @@ export class ReactLoopAgent implements Agent {
|
||||
outcome.failure, requestFailureHistory, outcome.retryPolicy, signal,
|
||||
() => Promise.resolve<RequestErrorAction>(undefined),
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- signal can abort while recovery is awaited.
|
||||
if (action?.kind === 'retry' && !signal.aborted) {
|
||||
retryFailures = Object.freeze([...requestFailureHistory, outcome.failure])
|
||||
}
|
||||
@@ -584,7 +584,7 @@ export class ReactLoopAgent implements Agent {
|
||||
const maxTokens = this.options.maxTokens
|
||||
const seedConfig = deepFreeze(structuredClone(
|
||||
this.requestHeaderLogged
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the header it now folds
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- the instance logged the header it now folds
|
||||
? persistedConfig!
|
||||
: {
|
||||
...route,
|
||||
|
||||
@@ -83,7 +83,7 @@ export async function executeToolCalls(
|
||||
let concluded = false
|
||||
while (next < planned.length) {
|
||||
// Commit before classifying again so registry changes affect unstarted calls.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
|
||||
const first = planned[next]!
|
||||
const mode = ctx.tools.executionMode(first.exec).kind
|
||||
const group = mode === 'parallel' ? planned.slice(next) : [first]
|
||||
@@ -151,7 +151,7 @@ async function runGroup(
|
||||
const result = slot.needsPost
|
||||
? await ctx.tools[TOOL_REGISTRY_SCHEDULER].finalize(slot.exec, slot.result)
|
||||
: ctx.tools[TOOL_REGISTRY_SCHEDULER].finish(slot.exec, slot.result)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded index
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded index
|
||||
appendToolResult(session, turn, step, call!.block, result, callSeqs[committed]!)
|
||||
for (const context of result.additionalContexts ?? []) acceptContext(context)
|
||||
concluded ||= result.concludesTurn === true
|
||||
@@ -162,7 +162,7 @@ async function runGroup(
|
||||
const inFlight = new Map<number, Promise<number>>()
|
||||
|
||||
const startCall = async (index: number): Promise<void> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded index
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded index
|
||||
const call = group[index]!
|
||||
callSeqs[index] = appendToolCall(session, turn, step, call.block)
|
||||
started++
|
||||
@@ -198,7 +198,7 @@ async function runGroup(
|
||||
const fillPool = async (): Promise<void> => {
|
||||
while (!aborted && nextToStart < group.length && inFlight.size < maxParallelToolCalls) {
|
||||
// Re-read later modes after ordered commits so registry changes can create a barrier.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
|
||||
const nextCall = group[nextToStart]!
|
||||
if (nextToStart > 0 && mode === 'parallel'
|
||||
&& ctx.tools.executionMode(nextCall.exec).kind !== 'parallel') break
|
||||
|
||||
@@ -269,7 +269,7 @@ describe('config-driven session id', () => {
|
||||
const failures: unknown[] = []
|
||||
ctx.on('agent-loop/config-start-failed', () => { throw unrenderable })
|
||||
// Deliberately violate the normal Error-only rejection rule to exercise the unknown boundary.
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
||||
ctx.on('agent-loop/config-start-failed', () => Promise.reject(unrenderable) as never)
|
||||
ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) })
|
||||
vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(unrenderable)
|
||||
|
||||
@@ -108,12 +108,12 @@ export function agentEvents(ctx: Context, agent: Agent): AgentEventDispatch {
|
||||
}
|
||||
},
|
||||
async serial(name, ...rest) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
// oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
const serial = ctx.serial as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => Promise<never>
|
||||
return await serial(carrier, name, agent, ...rest)
|
||||
},
|
||||
waterfall(name, ...rest) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
// oxlint-disable-next-line typescript/unbound-method -- the events mixin accessor returns a pre-bound function
|
||||
const waterfall = ctx.waterfall as (thisArg: Scoped<Agent>, name: string, ...args: unknown[]) => never
|
||||
return waterfall(carrier, name, agent, ...rest)
|
||||
},
|
||||
|
||||
@@ -328,7 +328,7 @@ export class AgentRegistry extends Service {
|
||||
// caller's composite effect can yield it for in-order teardown; the
|
||||
// loop's constructor effect returns it directly, identity-nesting the
|
||||
// registration under that effect.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ export class AgentRegistry extends Service {
|
||||
// capability and need no Cordis tracker magic.
|
||||
const { target } = this.requireFactory()
|
||||
const receiver = getTraceable(ownerCtx, target)
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
||||
// oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
||||
return Reflect.apply(target.createAgent, receiver, [ownerCtx, options])
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ export class AgentRegistry extends Service {
|
||||
const ownerCtx = this.ctx
|
||||
const { target } = this.requireFactory()
|
||||
const receiver = getTraceable(ownerCtx, target)
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
||||
// oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
|
||||
return Reflect.apply(target.resume, receiver, [ownerCtx, options])
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ export class AgentRegistry extends Service {
|
||||
yield this.enter(agent, this.ctx.agent)
|
||||
this.announce(agent)
|
||||
}.bind(this), 'agents.register()')
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,10 @@ export type AgentCancelCause =
|
||||
/** Runtime reason carried by the signal that controls one live turn. */
|
||||
export type AgentInterruptReason = AgentCancelCause | { readonly kind: 'disposed' }
|
||||
|
||||
/** Public live-agent handle with aliases over the unified delivery primitive. */
|
||||
/**
|
||||
* Public live-agent handle with aliases over the unified delivery primitive.
|
||||
* @typert object
|
||||
*/
|
||||
export interface Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
readonly id: SessionId
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* Contract and negative-path tests for the cordis catalog generator
|
||||
* (`scripts/gen-cordis-catalog.ts`).
|
||||
*/
|
||||
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectEvents, collectServices, renderEvents, renderServices } from '../../../../scripts/gen-cordis-catalog.ts'
|
||||
|
||||
/** Write a fixture package exposing one `interface Events` block and return the
|
||||
* scan root to hand `collectEvents`. */
|
||||
function fixtureRoot(eventsBlock: string): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cordis-catalog-'))
|
||||
const dir = join(root, 'packages', 'group', 'fix', 'src')
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(dir, 'index.ts'),
|
||||
`declare module 'cordis' {\n interface Events {\n${eventsBlock}\n }\n}\n`,
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
/** Write a fixture package exposing one `interface Context` entry (`ctx.fix` →
|
||||
* `FixService`) plus the class source, and return the scan root to hand
|
||||
* `collectServices`. */
|
||||
function serviceFixtureRoot(classSource: string): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cordis-catalog-'))
|
||||
const dir = join(root, 'packages', 'group', 'fix', 'src')
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(dir, 'index.ts'),
|
||||
`declare module 'cordis' {\n interface Context {\n fix: FixService\n }\n}\n\n${classSource}\n`,
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
const roots: string[] = []
|
||||
const make = (block: string): string => {
|
||||
const r = fixtureRoot(block)
|
||||
roots.push(r)
|
||||
return r
|
||||
}
|
||||
const makeService = (classSource: string): string => {
|
||||
const r = serviceFixtureRoot(classSource)
|
||||
roots.push(r)
|
||||
return r
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('gen-cordis-catalog collectEvents', () => {
|
||||
it('extracts a well-formed event with its @mode and JSDoc', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
expect(events[0]).toMatchObject({ name: 'fix/happened', scope: 'fix', mode: 'emit', doc: 'A thing happened.' })
|
||||
expect(events[0]?.jsDoc).toBe('/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */')
|
||||
expect(renderEvents(events)).toContain("```ts cordis-catalog\n/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n'fix/happened'(id: string): void\n```")
|
||||
})
|
||||
|
||||
it('classifies a trailing-next signature as a waterfall', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Intercept it.\n * @param x - the value under interception.\n * @mode waterfall\n */\n \'fix/intercept\'(x: number, next: () => Promise<number>): Promise<number>',
|
||||
))
|
||||
expect(events[0]?.mode).toBe('waterfall')
|
||||
})
|
||||
|
||||
it('accepts a parallel (awaited, no next) event by trusting the tag', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Flush.\n * @mode parallel\n */\n \'fix/flush\'(): Promise<void> | void',
|
||||
))
|
||||
expect(events[0]?.mode).toBe('parallel')
|
||||
})
|
||||
|
||||
it('accepts linked, foundation, generic-parameter, and explicitly exempt signature types', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata outside the core catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'<T extends SessionEvent>(value: Readonly<T>, preset: PresetSpec, signal: AbortSignal): Promise<T>',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
expect(renderEvents(events)).toContain('Types: [SessionEvent](../core-data-structures/core.md)')
|
||||
expect(renderEvents(events)).not.toContain('[PresetSpec]')
|
||||
})
|
||||
|
||||
it('aggregates every unclassified signature type with its source and remediation', () => {
|
||||
const expected = new RegExp([
|
||||
'2 signature type-link coverage violation\\(s\\)',
|
||||
'fix/one',
|
||||
'packages/group/fix/src/index.ts',
|
||||
'MissingOne',
|
||||
'fix/two',
|
||||
'packages/group/fix/src/index.ts',
|
||||
'missingTwo',
|
||||
'Add it to LINK_MAP',
|
||||
'FOUNDATION_TYPE_NAMES',
|
||||
'TYPE_LINK_EXEMPTIONS',
|
||||
].join('[\\s\\S]*'))
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * First.\n * @param value - first value.\n * @mode emit\n */\n \'fix/one\'(value: MissingOne): void\n /**\n * Second.\n * @param value - second value.\n * @mode emit\n */\n \'fix/two\'(value: missingTwo): void',
|
||||
))).toThrow(expected)
|
||||
})
|
||||
|
||||
it('hard-errors when an event is missing its @mode tag', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /** No mode here. */\n \'fix/untagged\'(): void',
|
||||
))).toThrow(/missing an @mode tag/)
|
||||
})
|
||||
|
||||
it('hard-errors when @mode contradicts a trailing-next (waterfall) shape', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * Mislabeled.\n * @param x - the value.\n * @mode emit\n */\n \'fix/wrong\'(x: number, next: () => Promise<number>): Promise<number>',
|
||||
))).toThrow(/trailing 'next' parameter .* tagged '@mode emit'/)
|
||||
})
|
||||
|
||||
it('hard-errors when @mode waterfall has no trailing next to delegate to', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * Not actually a waterfall.\n * @param id - which thing.\n * @mode waterfall\n */\n \'fix/nonext\'(id: string): void',
|
||||
))).toThrow(/tagged '@mode waterfall' but has no trailing 'next'/)
|
||||
})
|
||||
|
||||
it('hard-errors on an undocumented payload parameter', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/is missing @param id/)
|
||||
})
|
||||
|
||||
it('hard-errors on a stale @param naming no real parameter', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id - which thing.\n * @param ghost - not a parameter.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/@param ghost does not match any parameter/)
|
||||
})
|
||||
|
||||
it('hard-errors on an @param with an empty description', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/@param id has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on an event whose JSDoc has no description prose', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * @param id - which thing.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/no description prose/)
|
||||
})
|
||||
|
||||
it('exempts the `this` receiver and the trailing waterfall `next` from @param', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Scoped interception.\n * @param x - the value under interception.\n * @mode waterfall\n */\n \'fix/scoped\'(this: object, x: number, next: () => Promise<number>): Promise<number>',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('hard-errors on a binding-pattern parameter @param cannot name', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @mode emit\n */\n \'fix/destructured\'({ id }: { id: string }): void',
|
||||
))).toThrow(/is a binding pattern/)
|
||||
})
|
||||
|
||||
it('aggregates every violation into one error instead of failing fast', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /** First. */\n \'fix/one\'(): void\n /** Second. */\n \'fix/two\'(): void',
|
||||
))).toThrow(/2 JSDoc completeness violation\(s\)[\s\S]*fix\/one[\s\S]*fix\/two/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gen-cordis-catalog collectServices', () => {
|
||||
const WELL_FORMED = `/** Fixture service. */
|
||||
export class FixService {
|
||||
/**
|
||||
* Do the thing.
|
||||
* @param id - which thing to do.
|
||||
* @returns the outcome of doing it.
|
||||
*/
|
||||
run(id: string): string { return id }
|
||||
|
||||
/** Fire and forget (void needs no @returns). */
|
||||
poke(): void {}
|
||||
|
||||
/** Flush (Promise<void> needs no @returns either). */
|
||||
flush(): Promise<void> { return Promise.resolve() }
|
||||
}`
|
||||
|
||||
it('extracts a well-formed service with its methods and class JSDoc', () => {
|
||||
const services = collectServices(makeService(WELL_FORMED))
|
||||
expect(services).toHaveLength(1)
|
||||
expect(services[0]).toMatchObject({ key: 'fix', type: 'FixService', abstract: false, doc: 'Fixture service.' })
|
||||
expect(services[0]?.methods).toHaveLength(3)
|
||||
expect(services[0]?.methods[0]).toEqual({
|
||||
signature: 'run(id: string): string',
|
||||
jsDoc: '/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */',
|
||||
})
|
||||
expect(renderServices(services)).toContain('```ts cordis-catalog\n/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */\nrun(id: string): string\n\n/** Fire and forget (void needs no @returns). */\npoke(): void')
|
||||
})
|
||||
|
||||
it('hard-errors on an unclassified service-method signature type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Use an unknown value.\n * @param value - the value.\n */\n run(value: MissingServiceType): void {}\n}',
|
||||
))).toThrow(/service method ctx\.fix\.run .* references unclassified type 'MissingServiceType'/)
|
||||
})
|
||||
|
||||
it('hard-errors on a public method with no JSDoc at all', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/ctx\.fix\.run .* has no JSDoc/)
|
||||
})
|
||||
|
||||
it('hard-errors on an undocumented method parameter', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @returns the outcome.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/ctx\.fix\.run .* is missing @param id/)
|
||||
})
|
||||
|
||||
it('hard-errors on a missing @returns for a non-void return type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/is missing @returns \(return type: string\)/)
|
||||
})
|
||||
|
||||
it('hard-errors on an unannotated (inferred) return type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n */\n run(id: string) { return id }\n}',
|
||||
))).toThrow(/no return type annotation/)
|
||||
})
|
||||
|
||||
it('hard-errors on a service class with no JSDoc', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'export class FixService {\n /** Fire and forget. */\n poke(): void {}\n}',
|
||||
))).toThrow(/class FixService has no JSDoc/)
|
||||
})
|
||||
|
||||
it('hard-errors on a stale method @param', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Fire and forget.\n * @param ghost - not a parameter.\n */\n poke(): void {}\n}',
|
||||
))).toThrow(/@param ghost does not match any parameter/)
|
||||
})
|
||||
|
||||
it('hard-errors on a method whose JSDoc is tags with no description prose', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * @param id - which thing.\n * @returns the outcome.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/no description prose above its block tags/)
|
||||
})
|
||||
|
||||
it('hard-errors on a method @param with an empty description', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Fire and forget.\n * @param id\n */\n poke(id: string): void {}\n}',
|
||||
))).toThrow(/@param id has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on an @returns with an empty description', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n * @returns\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/@returns has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on a binding-pattern method parameter @param cannot name', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n */\n run({ id }: { id: string }): void {}\n}',
|
||||
))).toThrow(/is a binding pattern/)
|
||||
})
|
||||
|
||||
it('ignores private/protected/static members (not the ctx.<key> surface)', () => {
|
||||
const services = collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n private hidden(id: string): string { return id }\n protected hook(): void {}\n static helper(): void {}\n}',
|
||||
))
|
||||
expect(services[0]?.methods).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -241,7 +241,7 @@ export class ScopedLayers<L extends ScopeLayer> {
|
||||
}
|
||||
if (notify) this.onChange()
|
||||
}.bind(this), options.label)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- exact synchronous disposer preserves Cordis effect identity
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- exact synchronous disposer preserves Cordis effect identity
|
||||
return dispose
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +353,7 @@ const attachments = new WeakMap<Session, SessionEntry>()
|
||||
*
|
||||
* Plain class (not a Service) — create instances via `ctx.sessions.create()`.
|
||||
* Seeding with an existing event log replays/forks a session.
|
||||
* @typert object
|
||||
*/
|
||||
export class Session {
|
||||
private log: SessionEvent[] = []
|
||||
@@ -596,7 +597,7 @@ export class Session {
|
||||
for (const seq of nodes.slice(this.derivedNodes)) {
|
||||
// Surface sequences are built from this.log — seq is always a valid
|
||||
// index by construction. The non-null assertion expresses that invariant.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const msg = this.deriveEventMessage(this.log[seq]!)
|
||||
// A surface node is one of the five message-producing types, but an
|
||||
// empty-content assistant/message (a max-tokens step that hosts only
|
||||
@@ -911,7 +912,7 @@ export class SessionStore extends Service {
|
||||
} catch (error: unknown) {
|
||||
// Preserve the listener's exact rejection value; flush is a caller-owned
|
||||
// failure boundary, and Cordis listeners may throw arbitrary values.
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -340,7 +340,7 @@ export class SurfaceManager implements SessionSurface {
|
||||
/** Fold events appended since the previous access. */
|
||||
private _processDelta(): void {
|
||||
for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
|
||||
applySurfaceEvent(this._state, this.log[i]!, i, this.log)
|
||||
this._lastProcessedSeq = i
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('packChunkRuns', () => {
|
||||
['a block-index switch', [...deltaRun('text-delta', 2), ...deltaRun('text-delta', 1, 2, 7)]],
|
||||
['a step switch', deltaRun('text-delta', 3).map((e, k) => k === 2 ? chunkEvent(e.seq, e.time, (e.data as { chunk: StreamChunk }).chunk, 1, 2) : e)],
|
||||
])('breaks a run on %s (both halves too short to pack)', (_label, events) => {
|
||||
expect(packChunkRuns(events as SessionEvent[])).toStrictEqual(events)
|
||||
expect(packChunkRuns(events)).toStrictEqual(events)
|
||||
})
|
||||
|
||||
it('breaks a tool-call run on call-id or name change', () => {
|
||||
|
||||
@@ -546,19 +546,19 @@ export function defineTool<const S extends ParameterSchemaSpec, const O extends
|
||||
options: DefineToolOptions<S, O>,
|
||||
): ToolDefinition {
|
||||
// Object-literal methods do not use `this`; retaining references is safe.
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userExecute = options.execute
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userFinalizeContent = options.finalizeContent
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userRender = options.output.render
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userPresentationMeta = options.output.presentationMeta
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userPresentCall = options.presentCall
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userPresentResult = options.presentResult
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const userIsConcurrencySafe = options.isConcurrencySafe
|
||||
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
|
||||
throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`)
|
||||
|
||||
@@ -27,7 +27,7 @@ export type ContentToolFixtureOptions<S extends ParameterSchemaSpec> = Omit<
|
||||
export function defineContentToolFixture<const S extends ParameterSchemaSpec>(
|
||||
options: ContentToolFixtureOptions<S>,
|
||||
): ToolDefinition {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
const execute = options.execute
|
||||
return defineTool({
|
||||
...options,
|
||||
|
||||
Reference in New Issue
Block a user