refactor: narrow synchronous extension contracts
This commit is contained in:
@@ -13,9 +13,9 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool-
|
||||
|
||||
### Public API
|
||||
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => Promise<void> | void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. Duplicate names within one layer and non-finite orders throw. `ownerFinal: true` restores this section's canonical definition after the complete waterfall and reserves a global section against scoped shadows. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => Promise<void> | void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames?, ownerFinalNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`; `ownerFinalNames` makes those tools' canonical presence or absence survive the waterfall. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => Promise<void> | void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. Duplicate names within one layer and non-finite orders throw. `ownerFinal: true` restores this section's canonical definition after the complete waterfall and reserves a global section against scoped shadows. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames?, ownerFinalNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`; `ownerFinalNames` makes those tools' canonical presence or absence survive the waterfall. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform seam. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores owner-final contributions from a private pre-waterfall snapshot. Restored entries keep canonical relative order without undoing listener reordering of ordinary entries. Rejects when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
|
||||
|
||||
### Live events
|
||||
|
||||
@@ -434,7 +434,7 @@ export class SystemPrompt extends Service {
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
section(section: PromptSection): () => Promise<void> | void {
|
||||
section(section: PromptSection): () => void {
|
||||
if (!Number.isFinite(section.order)) {
|
||||
throw new TypeError(`prompt section "${section.name}" order must be a finite number`)
|
||||
}
|
||||
@@ -481,8 +481,9 @@ export class SystemPrompt extends Service {
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
// (the agents.register() lesson). Cleanup is synchronous because this
|
||||
// registration installs only synchronous state and notifications.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -502,7 +503,7 @@ export class SystemPrompt extends Service {
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
tools(provider: (context: AssembleContext) => ToolProviderResult): () => Promise<void> | void {
|
||||
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void {
|
||||
const scope = scopeOf(this.ctx)
|
||||
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
|
||||
const layer = scope === undefined
|
||||
@@ -527,8 +528,9 @@ export class SystemPrompt extends Service {
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
// (the agents.register() lesson). Cleanup is synchronous because this
|
||||
// registration installs only synchronous state and notifications.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
@@ -550,7 +552,7 @@ export class SystemPrompt extends Service {
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => Promise<void> | void {
|
||||
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void {
|
||||
if (!VARIABLE_NAME.test(name)) {
|
||||
throw new Error(`invalid prompt variable name "${name}" (must match ${String(VARIABLE_NAME)})`)
|
||||
}
|
||||
@@ -581,8 +583,9 @@ export class SystemPrompt extends Service {
|
||||
// effect that owns a teardown ORDER must be able to yield THIS function —
|
||||
// cordis nests a disposer out of the fiber's concurrent sibling list by
|
||||
// exact function identity, so a wrapper would silently break the nesting
|
||||
// (the agents.register() lesson). Fire-and-forget callers may still
|
||||
// discard the (always-resolved) promise.
|
||||
// (the agents.register() lesson). Cleanup is synchronous because this
|
||||
// registration installs only synchronous state and notifications.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('scoped tool providers and toolOrder × restriction', () => {
|
||||
const ctx = await mount()
|
||||
const scope = await mintScope(ctx, 'child')
|
||||
const dispose = scope.ctx.systemPrompt.tools(() => ({ schemas: [schema('scoped_tool')] }))
|
||||
await dispose()
|
||||
dispose()
|
||||
const after = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
|
||||
expect(after.tools.map(t => t.name)).toEqual([])
|
||||
// Re-registering through the same scope starts a fresh layer.
|
||||
|
||||
@@ -316,7 +316,7 @@ describe('SystemPrompt', () => {
|
||||
// registration emits change
|
||||
expect(changeCount).toBe(1)
|
||||
|
||||
await dispose()
|
||||
dispose()
|
||||
// disposal emits change again
|
||||
expect(changeCount).toBe(2)
|
||||
})
|
||||
@@ -341,7 +341,7 @@ describe('SystemPrompt', () => {
|
||||
const dispose = ctx.systemPrompt.section({ name: 'direct', order: 0, text: 'direct section' })
|
||||
expect(contributed(await ctx.systemPrompt.assemble())).toHaveLength(1)
|
||||
|
||||
await dispose()
|
||||
dispose()
|
||||
expect(contributed(await ctx.systemPrompt.assemble())).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -352,7 +352,7 @@ describe('SystemPrompt', () => {
|
||||
const dispose = ctx.systemPrompt.tools(() => ({ schemas: [{ name: 'direct-tool', description: '', parameters: {} }] }))
|
||||
expect((await ctx.systemPrompt.assemble()).tools).toHaveLength(1)
|
||||
|
||||
await dispose()
|
||||
dispose()
|
||||
expect((await ctx.systemPrompt.assemble()).tools).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -370,7 +370,7 @@ describe('SystemPrompt', () => {
|
||||
// A provider returning undefined records "registered but no value here".
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toEqual({ who: undefined })
|
||||
|
||||
await dispose()
|
||||
dispose()
|
||||
expect(changeCount).toBe(2)
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toEqual({})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user