fix: allow concurrent and unrestricted Runtime sessions
Local embedded OpenCode serialized otherwise independent conversations, and GoodBuddy could stop long Runtime tasks after fixed tool, activity, model-call, or accumulated display-output limits. Different conversations can now run concurrently while requests in the same conversation stay ordered. Dynamic MCP tools remain isolated to the current request. OpenCode, Continue, managed ACP, and the Agent model gateway no longer stop native execution at GoodBuddy-defined call counts or prompt-wide output totals; display copies truncate and transport queues apply backpressure instead. Prompt-wide quota fields are removed from the current contracts. Per-call model settings, provider request timeouts, cancellation, and same-conversation ordering remain unchanged. Release note: 修复本机 OpenCode 不同会话互相等待的问题,并移除 GoodBuddy 对单次 Runtime 工具、活动和模型调用次数的人为上限;长输出现在只截断展示或施加传输背压,不再取消原生任务。
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
builtInDefaultProjectSeedDescription,
|
||||
builtInDefaultProjectSeedName,
|
||||
conversationBranchInputSchema,
|
||||
conversationMessageSchema,
|
||||
conversationSubagentActivitySchema,
|
||||
conversationSnapshotSchema,
|
||||
isUntouchedBuiltInDefaultProject,
|
||||
@@ -172,6 +173,52 @@ describe('project execution space contracts', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('conversation activity contracts', () => {
|
||||
it('accepts more than 100 tool and subagent activities', () => {
|
||||
const tools = Array.from({ length: 101 }, (_, index) => ({
|
||||
callId: `call-${index + 1}`,
|
||||
name: 'read',
|
||||
state: 'completed' as const,
|
||||
summary: `Tool ${index + 1}`
|
||||
}))
|
||||
const subagents = Array.from({ length: 101 }, (_, index) => ({
|
||||
childTaskId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`,
|
||||
routingMode: 'native' as const,
|
||||
actor: {
|
||||
kind: 'direct-model' as const,
|
||||
label: '编程 Subagent' as const
|
||||
},
|
||||
state: 'completed' as const
|
||||
}))
|
||||
|
||||
expect(
|
||||
conversationMessageSchema.parse({
|
||||
id: '00000000-0000-4000-8000-000000000999',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
blocks: tools.map((tool, index) => ({
|
||||
id: `10000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`,
|
||||
type: 'tool',
|
||||
tool
|
||||
})),
|
||||
createdAt: 1,
|
||||
state: 'complete',
|
||||
tools,
|
||||
subagents
|
||||
})
|
||||
).toMatchObject({
|
||||
tools: expect.arrayContaining([
|
||||
expect.objectContaining({ callId: 'call-101' })
|
||||
]),
|
||||
subagents: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
childTaskId: '00000000-0000-4000-8000-000000000101'
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('conversation branch contracts', () => {
|
||||
it('accepts bounded branch provenance and rejects renderer extras', () => {
|
||||
const sourceConversationId =
|
||||
|
||||
@@ -182,9 +182,9 @@ export const conversationMessageBlockSchema = z.discriminatedUnion('type', [
|
||||
.strict()
|
||||
])
|
||||
|
||||
export const conversationMessageBlocksSchema = z
|
||||
.array(conversationMessageBlockSchema)
|
||||
.max(500)
|
||||
export const conversationMessageBlocksSchema = z.array(
|
||||
conversationMessageBlockSchema
|
||||
)
|
||||
|
||||
export type ConversationToolActivity = z.infer<
|
||||
typeof conversationToolActivitySchema
|
||||
@@ -193,9 +193,6 @@ export type ConversationMessageBlock = z.infer<
|
||||
typeof conversationMessageBlockSchema
|
||||
>
|
||||
|
||||
export const maximumConversationToolActivities = 100
|
||||
export const maximumConversationSubagentActivities = 100
|
||||
|
||||
export const subagentActorSchema = z
|
||||
.object({
|
||||
kind: z.literal('direct-model'),
|
||||
@@ -277,14 +274,8 @@ export const conversationMessageSchema = z
|
||||
.array(conversationContextCompressionMarkerSchema)
|
||||
.max(2)
|
||||
.optional(),
|
||||
tools: z
|
||||
.array(conversationToolActivitySchema)
|
||||
.max(maximumConversationToolActivities)
|
||||
.optional(),
|
||||
subagents: z
|
||||
.array(conversationSubagentActivitySchema)
|
||||
.max(maximumConversationSubagentActivities)
|
||||
.optional(),
|
||||
tools: z.array(conversationToolActivitySchema).optional(),
|
||||
subagents: z.array(conversationSubagentActivitySchema).optional(),
|
||||
sources: z.array(z.string().max(8_192)).max(100).optional(),
|
||||
sourceReferences: z
|
||||
.array(
|
||||
|
||||
@@ -191,14 +191,14 @@ describe('model bridge contracts and codec', () => {
|
||||
apiKey: 'must-not-cross'
|
||||
})
|
||||
).toThrow()
|
||||
expect(
|
||||
expect(() =>
|
||||
modelBridgePolicySchema.parse({
|
||||
...policy,
|
||||
maximumOutputTokens: 65_537,
|
||||
maximumModelCalls: 8,
|
||||
maximumTotalOutputTokens: 32_768
|
||||
})
|
||||
).toMatchObject(policy)
|
||||
).toThrow()
|
||||
await expect(
|
||||
createModelBridgeRequestMessage({
|
||||
identity,
|
||||
|
||||
@@ -126,8 +126,6 @@ export const agentPromptModelProfileSchema = z
|
||||
limits: z
|
||||
.object({
|
||||
maximumOutputTokens: z.number().int().min(1).max(1_000_000),
|
||||
maximumModelCalls: z.number().int().min(1).max(10_000),
|
||||
maximumTotalOutputTokens: z.number().int().min(1).max(10_000_000),
|
||||
requestTimeoutMilliseconds: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -158,16 +156,6 @@ export const agentPromptModelProfileSchema = z
|
||||
message: 'Model provider does not match its wire protocol'
|
||||
})
|
||||
}
|
||||
if (
|
||||
profile.limits.maximumOutputTokens >
|
||||
profile.limits.maximumTotalOutputTokens
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['limits', 'maximumOutputTokens'],
|
||||
message: 'Per-call output limit exceeds the prompt total'
|
||||
})
|
||||
}
|
||||
})
|
||||
export type AgentPromptModelProfile = z.infer<
|
||||
typeof agentPromptModelProfileSchema
|
||||
@@ -178,11 +166,6 @@ export const modelBridgePolicySchema = z
|
||||
protocol: modelBridgeModelProtocolSchema,
|
||||
model: modelNameSchema,
|
||||
modelProfileDigest: sha256DigestSchema,
|
||||
// Accepted only so persisted pre-removal bindings can be read and
|
||||
// retired normally. Current code neither emits nor enforces these fields.
|
||||
maximumOutputTokens: z.number().int().min(1).optional(),
|
||||
maximumModelCalls: z.number().int().min(1).optional(),
|
||||
maximumTotalOutputTokens: z.number().int().min(1).optional(),
|
||||
supportsImageInput: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
@@ -116,20 +116,20 @@ describe('model call operation contracts', () => {
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('accepts legacy quota fields without requiring them', () => {
|
||||
it('rejects prompt-wide quota fields', () => {
|
||||
const request = {
|
||||
identity,
|
||||
requestDigest: `sha256:${'a'.repeat(64)}`,
|
||||
...policy
|
||||
}
|
||||
expect(prepareModelCallSchema.parse(request)).toEqual(request)
|
||||
expect(
|
||||
expect(() =>
|
||||
prepareModelCallSchema.parse({
|
||||
...request,
|
||||
maximumOutputTokens: 65_537,
|
||||
maximumModelCalls: 100_000,
|
||||
maximumTotalOutputTokens: 1_000_000_000
|
||||
})
|
||||
).toMatchObject(request)
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,12 +60,7 @@ export const prepareModelCallSchema = z
|
||||
.object({
|
||||
identity: modelCallIdentitySchema,
|
||||
requestDigest: modelCallDigestSchema,
|
||||
modelProfileDigest: modelCallDigestSchema,
|
||||
// Legacy ledger fields are accepted during migration but no longer
|
||||
// constrain dispatch.
|
||||
maximumOutputTokens: positiveCountSchema.optional(),
|
||||
maximumModelCalls: positiveCountSchema.optional(),
|
||||
maximumTotalOutputTokens: positiveCountSchema.optional()
|
||||
modelProfileDigest: modelCallDigestSchema
|
||||
})
|
||||
.strict()
|
||||
export type PrepareModelCall = z.infer<typeof prepareModelCallSchema>
|
||||
@@ -164,9 +159,6 @@ export const modelCallRecordSchema = z
|
||||
identity: modelCallIdentitySchema,
|
||||
requestDigest: modelCallDigestSchema,
|
||||
modelProfileDigest: modelCallDigestSchema,
|
||||
maximumOutputTokens: positiveCountSchema.optional(),
|
||||
maximumModelCalls: positiveCountSchema.optional(),
|
||||
maximumTotalOutputTokens: positiveCountSchema.optional(),
|
||||
status: modelCallStatusSchema,
|
||||
dispatchMetadata: modelCallDispatchMetadataSchema.optional(),
|
||||
terminalEvidence: modelCallTerminalEvidenceSchema.optional(),
|
||||
|
||||
Reference in New Issue
Block a user