Merge remote-tracking branch 'origin/master' into xtr/identified-immutable-messages

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent/README.i18n.yaml
This commit is contained in:
_Kerman
2026-07-28 18:49:23 +08:00
59 changed files with 364 additions and 97 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
README.md: 99fdc73e2dcb09ed7d47f021598dea8720cada5d
README.zh.md: fe9db9a625ea289e7e9364a182061dee1a0e773c
README.md: 6bb8b12af69f54c2a75cd672e4d3802887808c76
README.zh.md: f9eb8aa3cdead427a88492e35c00eab80ba12f91

View File

@@ -42,13 +42,14 @@ interface Config {
id: string // required
provider?: string
model?: string
maxTokens?: number // positive per-request output-token cap
resumeSessionId?: string // load this persisted session instead of creating one
cwd?: string // optional workspace cwd for the fresh session
}>
}
```
Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`.
Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`.
### Internal concrete driver

View File

@@ -42,13 +42,14 @@ interface Config {
id: string // required
provider?: string
model?: string
maxTokens?: number // positive per-request output-token cap
resumeSessionId?: string // load this persisted session instead of creating one
cwd?: string // optional workspace cwd for the fresh session
}>
}
```
通过配置创建的 agent 会自动启动。模型调用同时需要 `provider``model``agent/request` 可以在分发前补齐缺失的这一对值。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10``cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona编程式 setup 可以按 agent 遮蔽它。该插件提供逐 agent 的 `provider``model``cwd` 提示词变量harness 身份与部署 persona 属于 `dsh-system-prompt`
通过配置创建的 agent 会自动启动。模型调用同时需要 `provider``model``agent/request` 可以在分发前补齐缺失的这一对值。可选的正整数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10``cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona编程式 setup 可以按 agent 遮蔽它。该插件提供逐 agent 的 `provider``model``cwd` 提示词变量harness 身份与部署 persona 属于 `dsh-system-prompt`
### 包内部实体驱动器

View File

@@ -581,11 +581,16 @@ export class ReactLoopAgent implements Agent {
&& persistedConfig.model === route.model
? persistedConfig.reasoningEffort
: undefined
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
? persistedConfig!
: { ...route, ...reasoningEffort === undefined ? {} : { reasoningEffort } },
: {
...route,
...reasoningEffort === undefined ? {} : { reasoningEffort },
...maxTokens === undefined ? {} : { maxTokens },
},
))
const proposedConfig = await this.loopCtx.waterfall(
agentCarrier(this), 'agent/request', this, turn, step, signal,

View File

@@ -112,6 +112,14 @@ function resolveMaxParallelToolCalls(value: number | undefined): number {
return maxParallelToolCalls
}
/** Reject an output-token cap that cannot be represented exactly on the request wire. */
function assertAgentOptions(options: AgentOptions): void {
if (options.maxTokens !== undefined
&& (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) {
throw new TypeError('agent maxTokens must be a positive safe integer')
}
}
/** Prepared-but-unpublished agent resources sharing one memoized teardown. */
interface PreparedAgent {
agent: ReactLoopAgent
@@ -196,6 +204,7 @@ export class AgentLoop extends Service implements AgentFactory {
sessionId: z.string().min(1),
provider: z.string(),
model: z.string(),
maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
cwd: z.string(),
resumeSessionId: z.string(),
})).default([]),
@@ -327,6 +336,7 @@ export class AgentLoop extends Service implements AgentFactory {
* fuses caller cancellation with lifecycle teardown for setup awaits.
*/
private prepare(ownerCtx: Context, id: SessionId, options: AgentOptions, session: Session, callerSignal?: AbortSignal): PreparedAgent {
assertAgentOptions(options)
ownerCtx.fiber.assertActive()
// Every caller reaches prepare() synchronously from a service method
// whose Cordis dispatch already requires the live factory fiber, or

View File

@@ -46,6 +46,19 @@ function send(agent: Agent, text: string) {
}
describe('agent loop', () => {
it.each([0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
'rejects invalid AgentOptions.maxTokens %s before publication',
async (maxTokens) => {
const ctx = await harness(new MockAdapter([]))
expect(() => ctx.agentLoop.create(
SessionId('invalid-max-tokens'),
{ provider: 'mock', model: 'mock', maxTokens },
)).toThrow('agent maxTokens must be a positive safe integer')
expect(ctx.agents.list()).toEqual([])
expect(ctx.sessions.list()).toEqual([])
},
)
it('runs a simple turn: queued message → model → idle, with ordered events', async () => {
const adapter = new MockAdapter([textResponse('hello there')])
const ctx = await harness(adapter)