@openchambery/web 1.19.14 → 1.19.15-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AssistantView-Bp4j2j2j.js +2 -0
- package/dist/assets/{DiagramView-CovEl1BY.js → DiagramView-OdVPLt6s.js} +1 -1
- package/dist/assets/MarkdownRenderer-4weeFDNg.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CCfHY-86.js → MarkdownRendererImpl-D9O0gsEc.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-y6QgIUZA.js → MarkstreamRendererImpl-ovXFLt9J.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BYzib11p.js +1 -0
- package/dist/assets/{MobileShareBridge-B4S7BaUj.js → MobileShareBridge-QUhimU7K.js} +1 -1
- package/dist/assets/MobileSurface-zxMVX0-y.js +2 -0
- package/dist/assets/{MultiRunWindow-BeHa-CG6.js → MultiRunWindow-CzG_tdV0.js} +1 -1
- package/dist/assets/{SettingsView-Dn7odtQz.js → SettingsView-Dr-d6ryG.js} +3 -3
- package/dist/assets/{SettingsWindow-C0tWPS5f.js → SettingsWindow-BI4lg5NY.js} +1 -1
- package/dist/assets/{TerminalView-BnJgC7Qo.js → TerminalView-C6Wqo-mQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-D9S-X8hM.js → ToolOutputDialog-DEIsCH48.js} +1 -1
- package/dist/assets/{appThemeRegistry-DBrvaJnq.js → appThemeRegistry-nDZxiGjG.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-Dc004xOh.js → fileMentionTouchSelection-C4twfQKY.js} +1 -1
- package/dist/assets/{gitApi-Cm3JLQqh.js → gitApi-BRZeUzHF.js} +1 -1
- package/dist/assets/{insertionBoundaries-CTWYFsbH.js → insertionBoundaries-CK0Nv-Vf.js} +1 -1
- package/dist/assets/{main-DwMprEyr.js → main-BRIfvcsR.js} +2 -2
- package/dist/assets/{main-CSqu3MB4.js → main-w3PEAPHR.js} +4 -4
- package/dist/assets/{miniChat-C8bzY_Ye.js → miniChat-DSuRbtqh.js} +2 -2
- package/dist/assets/{mobile-Oy-SRKkr.js → mobile-B9Ek0XAw.js} +2 -2
- package/dist/assets/{multirun-ClcuaiKU.js → multirun-Bh4P5g3q.js} +1 -1
- package/dist/assets/{projectMeta-F93wE2Ye.js → projectMeta-H0Ghy4GZ.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-rOOhMVlQ.js → renderElectronMiniChatApp-DJQvuapb.js} +2 -2
- package/dist/assets/renderMobileApp-BP2iltJl.js +28 -0
- package/dist/assets/{runtimeConfig-iJ2mTBVN.js → runtimeConfig-AZn89Pi3.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ISnhAS3q.js → runtimeEndpointReset-B86-1ID-.js} +1 -1
- package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
- package/dist/assets/{sessionLookup-DQQ9-DuP.js → sessionLookup-DqMZlsfX.js} +1 -1
- package/dist/assets/src-CnLJ1J1M.css +2 -0
- package/dist/assets/{useAppFontEffects--IcU7jyv.js → useAppFontEffects-DgMhWd3K.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BqJ5R_T2.js → useDesktopWindowControlsLayout-CEs-_qs8.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-ChWXV8xM.js → useEffectiveDirectory-LPMarhQP.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DQkgxuzg.js → useMobileNavigationStore-C02t_wmj.js} +2 -2
- package/dist/assets/{useSessionAutoCleanup-DWWAvQQ0.js → useSessionAutoCleanup-DdXvH0A6.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-D2oQszsk.js → useWorkerHighlightedLines-4HwUoeTn.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +33 -12
- package/server/lib/assistants/contact-store.js +25 -1
- package/server/lib/assistants/contact-store.test.js +39 -5
- package/server/lib/assistants/contact-tools.js +3 -3
- package/server/lib/assistants/harness.js +136 -101
- package/server/lib/assistants/harness.test.js +235 -8
- package/server/lib/assistants/pi-tools.js +2 -0
- package/server/lib/assistants/pi-tools.test.js +8 -0
- package/server/lib/assistants/service.js +313 -74
- package/server/lib/assistants/service.test.js +555 -18
- package/server/lib/llm/DOCUMENTATION.md +9 -4
- package/server/lib/llm/generate.js +45 -6
- package/server/lib/llm/generate.test.js +52 -0
- package/dist/assets/AssistantView-Dncbu_Z2.js +0 -2
- package/dist/assets/MarkdownRenderer-CZqok-Cf.js +0 -2
- package/dist/assets/MobileDetailNavigation-Ca-d5eZ9.js +0 -1
- package/dist/assets/MobileSurface-xL5Jby4U.js +0 -2
- package/dist/assets/renderMobileApp-BP_hGwHP.js +0 -28
- package/dist/assets/sessionChangeSummary-BAzU2J1H.js +0 -2
- package/dist/assets/src-DYeppLfP.css +0 -2
|
@@ -13,10 +13,47 @@ import {
|
|
|
13
13
|
NEW_CONVERSATION_TOOL_NAME,
|
|
14
14
|
createContactTools,
|
|
15
15
|
} from './contact-tools.js'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createContactStreamFn,
|
|
18
|
+
projectStreamedContactTurnBubbles,
|
|
19
|
+
resolveContactLanguage,
|
|
20
|
+
runContactTurn,
|
|
21
|
+
} from './harness.js'
|
|
17
22
|
import { PI_CODING_TOOL_NAMES } from './pi-tools.js'
|
|
18
23
|
|
|
19
24
|
describe('createContactStreamFn', () => {
|
|
25
|
+
it('runs managed contact lookups from the user home with the coordination policy', async () => {
|
|
26
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-contact-home-'))
|
|
27
|
+
const homeSpy = vi.spyOn(os, 'homedir').mockReturnValue(home)
|
|
28
|
+
fs.writeFileSync(path.join(home, 'example-config.txt'), 'configured value')
|
|
29
|
+
const createChatCompletion = vi.fn(async ({ body }) => {
|
|
30
|
+
const system = body.messages[0].content
|
|
31
|
+
expect(system).toContain(`Working directory: ${home}`)
|
|
32
|
+
expect(system).toContain('personable contact focused on understanding the user')
|
|
33
|
+
expect(system).toContain('default to finding the relevant existing OpenCode workspace')
|
|
34
|
+
expect(system).toContain('small, clearly scoped configuration changes directly')
|
|
35
|
+
expect(system).toContain('walk its parent directories')
|
|
36
|
+
expect(system).toContain('Look up existing OpenCode conversations with list_sessions')
|
|
37
|
+
const result = body.messages.find((message) => typeof message.content === 'string' && message.content.includes('tool result name=read'))
|
|
38
|
+
if (result) {
|
|
39
|
+
expect(result.content).toContain('configured value')
|
|
40
|
+
return { completion: { choices: [{ message: { content: '```openchamber-final\n{"status":"complete","text":"configured value"}\n```' } }] } }
|
|
41
|
+
}
|
|
42
|
+
return { completion: { choices: [{ message: { content: '```openchamber-tool\n{"name":"read","arguments":{"path":"example-config.txt"}}\n```' } }] } }
|
|
43
|
+
})
|
|
44
|
+
try {
|
|
45
|
+
const result = await runContactTurn({
|
|
46
|
+
assistant: { providerID: 'p', modelID: 'm', workspacePath: null, effectiveWorkspacePath: path.join(home, 'managed') },
|
|
47
|
+
history: [], userText: 'Read example-config.txt', skillHomeDir: home, createChatCompletion,
|
|
48
|
+
})
|
|
49
|
+
expect(result.text).toContain('configured value')
|
|
50
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
51
|
+
} finally {
|
|
52
|
+
homeSpy.mockRestore()
|
|
53
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
20
57
|
it('forwards completion text as pi-ai text events', async () => {
|
|
21
58
|
const createChatCompletion = vi.fn(async () => ({
|
|
22
59
|
completion: { choices: [{ message: { content: 'Hi' } }] },
|
|
@@ -293,6 +330,43 @@ describe('createContactStreamFn', () => {
|
|
|
293
330
|
{ index: 1, delta: 'Two.', done: true },
|
|
294
331
|
])
|
|
295
332
|
})
|
|
333
|
+
|
|
334
|
+
it('continues bubble indices after prior turn streamed assistant messages (tool then final)', async () => {
|
|
335
|
+
const bubbleDeltas = []
|
|
336
|
+
const createChatCompletion = vi.fn(async () => ({
|
|
337
|
+
completion: { choices: [{ message: { content: 'Final B.\n\nFinal C.' } }] },
|
|
338
|
+
}))
|
|
339
|
+
const streamFn = createContactStreamFn(createChatCompletion, {
|
|
340
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
341
|
+
turnMessageStart: 0,
|
|
342
|
+
})
|
|
343
|
+
const priorTurn = [
|
|
344
|
+
{ role: 'user', content: 'do it', timestamp: 1 },
|
|
345
|
+
{
|
|
346
|
+
role: 'assistant',
|
|
347
|
+
content: [
|
|
348
|
+
{ type: 'text', text: 'Preamble A' },
|
|
349
|
+
{ type: 'toolCall', id: 'call_1', name: 'assign_session', arguments: { prompt: 'x' } },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
role: 'toolResult',
|
|
354
|
+
toolName: 'assign_session',
|
|
355
|
+
toolCallId: 'call_1',
|
|
356
|
+
content: [{ type: 'text', text: 'Opened a coding session.' }],
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
expect(projectStreamedContactTurnBubbles(priorTurn, 0)).toEqual(['Preamble A'])
|
|
360
|
+
const stream = streamFn(
|
|
361
|
+
{ name: 'openai/gpt-5.2', id: 'gpt-5.2', provider: 'openchamber', api: 'openai-completions' },
|
|
362
|
+
{ messages: priorTurn },
|
|
363
|
+
)
|
|
364
|
+
for await (const event of stream) void event
|
|
365
|
+
expect(bubbleDeltas).toEqual([
|
|
366
|
+
{ index: 1, delta: 'Final B.', done: true },
|
|
367
|
+
{ index: 2, delta: 'Final C.', done: true },
|
|
368
|
+
])
|
|
369
|
+
})
|
|
296
370
|
})
|
|
297
371
|
|
|
298
372
|
describe('runContactTurn', () => {
|
|
@@ -607,8 +681,8 @@ describe('runContactTurn', () => {
|
|
|
607
681
|
createChatCompletion: vi.fn(), AgentImpl,
|
|
608
682
|
})
|
|
609
683
|
expect(prompts).toHaveLength(2)
|
|
610
|
-
// assign toolText is not a user bubble
|
|
611
|
-
expect(result.bubbles).toEqual([])
|
|
684
|
+
// assign toolText is not a user bubble. Turn-boundary keeps pre-retry assistant prose.
|
|
685
|
+
expect(result.bubbles).toEqual(['Only document search is available.'])
|
|
612
686
|
expect(result.bubbles.join('\n')).not.toContain('Opened session.')
|
|
613
687
|
})
|
|
614
688
|
it('runs pi-agent-core with thinking off and no tools by default', async () => {
|
|
@@ -786,7 +860,7 @@ describe('runContactTurn', () => {
|
|
|
786
860
|
expect(options.initialState.systemPrompt).toContain('说一声')
|
|
787
861
|
expect(options.initialState.systemPrompt).toContain('A reply without the tool call does nothing')
|
|
788
862
|
expect(options.initialState.systemPrompt).toContain('已创建')
|
|
789
|
-
expect(options.initialState.systemPrompt).toContain('
|
|
863
|
+
expect(options.initialState.systemPrompt).toContain('You receive the registered project catalog every turn')
|
|
790
864
|
this.state = { ...options.initialState, messages: [] }
|
|
791
865
|
this.prompt = async () => {
|
|
792
866
|
this.state.messages = [
|
|
@@ -1065,7 +1139,8 @@ describe('runContactTurn', () => {
|
|
|
1065
1139
|
name: 'FlowNL',
|
|
1066
1140
|
assistantID: 'asst_flow',
|
|
1067
1141
|
})])
|
|
1068
|
-
|
|
1142
|
+
// Turn-boundary keeps pre-retry spoken; English tool confirm still stays out.
|
|
1143
|
+
expect(result.bubbles.join('')).toContain('好的,我来直接创建')
|
|
1069
1144
|
expect(result.bubbles.join('')).not.toContain('Created assistant FlowNL')
|
|
1070
1145
|
expect(result.cards).toHaveLength(1)
|
|
1071
1146
|
expect(result.thinkingLevel).toBe('off')
|
|
@@ -1104,7 +1179,7 @@ describe('runContactTurn', () => {
|
|
|
1104
1179
|
expect(createAssistant).not.toHaveBeenCalled()
|
|
1105
1180
|
expect(result.cards).toEqual([])
|
|
1106
1181
|
// Prefer existing assistant text over the English missed-tool fallback.
|
|
1107
|
-
expect(result.bubbles).toEqual(['好的,已创建。'])
|
|
1182
|
+
expect(result.bubbles).toEqual(['好的,已创建。', '好的,已创建。'])
|
|
1108
1183
|
expect(result.bubbles).not.toEqual([MISSED_TOOL_FAILURE_BUBBLE])
|
|
1109
1184
|
})
|
|
1110
1185
|
|
|
@@ -1163,9 +1238,9 @@ describe('runContactTurn', () => {
|
|
|
1163
1238
|
text: 'hello-from-assistant 写好了',
|
|
1164
1239
|
})
|
|
1165
1240
|
expect(result.cards).toEqual([])
|
|
1166
|
-
// English tool confirm must not leak
|
|
1241
|
+
// English tool confirm must not leak. Turn-boundary projection keeps the pre-retry spoken line.
|
|
1167
1242
|
expect(result.bubbles.join('')).not.toContain('Sent to PeerQA')
|
|
1168
|
-
expect(result.bubbles.join('')).
|
|
1243
|
+
expect(result.bubbles.join('')).toContain('好的,我去说一声')
|
|
1169
1244
|
})
|
|
1170
1245
|
|
|
1171
1246
|
it('keeps only the new_conversation confirm and drops leftover attachment text', async () => {
|
|
@@ -1361,6 +1436,158 @@ describe('rejected response execution evidence', () => {
|
|
|
1361
1436
|
})
|
|
1362
1437
|
})
|
|
1363
1438
|
|
|
1439
|
+
describe('runContactTurn bubble index continuity', () => {
|
|
1440
|
+
const fence = (name, args) => '```openchamber-tool\n' + JSON.stringify({ name, arguments: args }) + '\n```'
|
|
1441
|
+
const completionFromReplies = (replies) => {
|
|
1442
|
+
const queue = [...replies]
|
|
1443
|
+
return vi.fn(async () => ({
|
|
1444
|
+
completion: { choices: [{ message: { content: queue.shift() || '' } }] },
|
|
1445
|
+
}))
|
|
1446
|
+
}
|
|
1447
|
+
const readSessionTools = (sessionID = 'ses_idx') => createContactTools({
|
|
1448
|
+
readSession: async () => ({
|
|
1449
|
+
sessionID,
|
|
1450
|
+
messages: [{ messageID: 'm1', role: 'user', parts: [{ type: 'text', text: 'hi' }] }],
|
|
1451
|
+
}),
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
it('real Agent gateway stub: preamble then read_session then multi-bubble final keeps stable SSE indices', async () => {
|
|
1455
|
+
const bubbleDeltas = []
|
|
1456
|
+
const createChatCompletion = completionFromReplies([
|
|
1457
|
+
`我去读一下\n\n${fence('read_session', { sessionID: 'ses_idx' })}`,
|
|
1458
|
+
'办好了。\n\n还有后续说明。',
|
|
1459
|
+
])
|
|
1460
|
+
const result = await runContactTurn({
|
|
1461
|
+
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1462
|
+
history: [],
|
|
1463
|
+
userText: '读一下那个会话',
|
|
1464
|
+
createChatCompletion,
|
|
1465
|
+
tools: readSessionTools(),
|
|
1466
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
1467
|
+
})
|
|
1468
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(2)
|
|
1469
|
+
const doneBubbles = bubbleDeltas.filter((item) => item.done)
|
|
1470
|
+
expect(doneBubbles.map((item) => ({ index: item.index, delta: item.delta }))).toEqual([
|
|
1471
|
+
{ index: 0, delta: '我去读一下' },
|
|
1472
|
+
{ index: 1, delta: '办好了。' },
|
|
1473
|
+
{ index: 2, delta: '还有后续说明。' },
|
|
1474
|
+
])
|
|
1475
|
+
expect(result.bubbles).toEqual(['我去读一下', '办好了。', '还有后续说明。'])
|
|
1476
|
+
// Published stream is a stable prefix of the final array.
|
|
1477
|
+
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles.map((item) => item.delta))
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
it('real Agent: two read_session preambles keep both spoken lines plus final in bubbles', async () => {
|
|
1481
|
+
const bubbleDeltas = []
|
|
1482
|
+
const createChatCompletion = completionFromReplies([
|
|
1483
|
+
`我去读一下\n\n${fence('read_session', { sessionID: 'ses_a' })}`,
|
|
1484
|
+
`我再读一下\n\n${fence('read_session', { sessionID: 'ses_b' })}`,
|
|
1485
|
+
'办好了',
|
|
1486
|
+
])
|
|
1487
|
+
const result = await runContactTurn({
|
|
1488
|
+
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1489
|
+
history: [],
|
|
1490
|
+
userText: '读两个会话',
|
|
1491
|
+
createChatCompletion,
|
|
1492
|
+
tools: readSessionTools('ses_a'),
|
|
1493
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
1494
|
+
})
|
|
1495
|
+
expect(createChatCompletion).toHaveBeenCalledTimes(3)
|
|
1496
|
+
const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
|
|
1497
|
+
expect(doneBubbles).toEqual(['我去读一下', '我再读一下', '办好了'])
|
|
1498
|
+
expect(result.bubbles).toEqual(['我去读一下', '我再读一下', '办好了'])
|
|
1499
|
+
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
it('real Agent: duplicate preamble text is kept as independent bubbles', async () => {
|
|
1503
|
+
const bubbleDeltas = []
|
|
1504
|
+
const createChatCompletion = completionFromReplies([
|
|
1505
|
+
`我去读一下\n\n${fence('read_session', { sessionID: 'ses_a' })}`,
|
|
1506
|
+
`我去读一下\n\n${fence('read_session', { sessionID: 'ses_b' })}`,
|
|
1507
|
+
'办好了',
|
|
1508
|
+
])
|
|
1509
|
+
const result = await runContactTurn({
|
|
1510
|
+
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1511
|
+
history: [],
|
|
1512
|
+
userText: '再读一遍',
|
|
1513
|
+
createChatCompletion,
|
|
1514
|
+
tools: readSessionTools('ses_a'),
|
|
1515
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
1516
|
+
})
|
|
1517
|
+
const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
|
|
1518
|
+
expect(doneBubbles).toEqual(['我去读一下', '我去读一下', '办好了'])
|
|
1519
|
+
expect(result.bubbles).toEqual(['我去读一下', '我去读一下', '办好了'])
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
it('real Agent: multi-segment spoken preambles then terminal tool confirm append after published prefix', async () => {
|
|
1523
|
+
const bubbleDeltas = []
|
|
1524
|
+
// Two independent short spokens (each tool turn); confirm-only tool text appends at end.
|
|
1525
|
+
// update_default_prompt is terminate:false so a plain final closes the agent loop.
|
|
1526
|
+
const createChatCompletion = completionFromReplies([
|
|
1527
|
+
`我先看一下\n\n${fence('list_sessions', { query: 'login' })}`,
|
|
1528
|
+
`马上改人设\n\n${fence('update_default_prompt', { prompt: 'be brief' })}`,
|
|
1529
|
+
'改好了',
|
|
1530
|
+
])
|
|
1531
|
+
const tools = createContactTools({
|
|
1532
|
+
listSessions: async () => ({ sessions: [] }),
|
|
1533
|
+
updateAssistantSettings: async ({ defaultPrompt }) => ({ defaultPrompt, updated: true }),
|
|
1534
|
+
currentAssistant: { id: 'asst_x', name: 'A', providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1535
|
+
})
|
|
1536
|
+
const result = await runContactTurn({
|
|
1537
|
+
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1538
|
+
history: [],
|
|
1539
|
+
userText: '改默认提示词 be brief',
|
|
1540
|
+
createChatCompletion,
|
|
1541
|
+
tools,
|
|
1542
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
1543
|
+
})
|
|
1544
|
+
const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
|
|
1545
|
+
expect(doneBubbles).toEqual(['我先看一下', '马上改人设', '改好了'])
|
|
1546
|
+
// Tool-only confirm appends after the published spoken/final prefix.
|
|
1547
|
+
expect(result.bubbles.slice(0, 3)).toEqual(['我先看一下', '马上改人设', '改好了'])
|
|
1548
|
+
expect(result.bubbles.length).toBeGreaterThan(3)
|
|
1549
|
+
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
|
|
1550
|
+
expect(result.bubbles.at(-1)).toMatch(/Default prompt/i)
|
|
1551
|
+
expect(result.bubbles.join('\n')).not.toContain('openchamber-tool')
|
|
1552
|
+
})
|
|
1553
|
+
|
|
1554
|
+
it('real Agent: missed-tool retry keeps pre-retry spoken bubbles in the final array', async () => {
|
|
1555
|
+
const bubbleDeltas = []
|
|
1556
|
+
const createAssistant = vi.fn(async (input) => ({
|
|
1557
|
+
id: 'asst_miss',
|
|
1558
|
+
name: input.name,
|
|
1559
|
+
providerID: input.providerID,
|
|
1560
|
+
modelID: input.modelID,
|
|
1561
|
+
mode: 'continuous',
|
|
1562
|
+
}))
|
|
1563
|
+
const createChatCompletion = completionFromReplies([
|
|
1564
|
+
'好的,我来创建。',
|
|
1565
|
+
`这就建好\n\n${fence('create_assistant', { name: 'MissRetry', model: 'p/m' })}`,
|
|
1566
|
+
])
|
|
1567
|
+
const result = await runContactTurn({
|
|
1568
|
+
assistant: { providerID: 'p', modelID: 'm', defaultPrompt: '' },
|
|
1569
|
+
history: [],
|
|
1570
|
+
userText: '帮我新建一个助理,名叫 MissRetry,不要开编码 session',
|
|
1571
|
+
createChatCompletion,
|
|
1572
|
+
tools: createContactTools({
|
|
1573
|
+
createAssistant,
|
|
1574
|
+
currentAssistant: { providerID: 'p', modelID: 'm' },
|
|
1575
|
+
}),
|
|
1576
|
+
onBubbleDelta: (index, delta, done) => bubbleDeltas.push({ index, delta, done }),
|
|
1577
|
+
})
|
|
1578
|
+
expect(createChatCompletion.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
1579
|
+
expect(createAssistant).toHaveBeenCalled()
|
|
1580
|
+
const doneBubbles = bubbleDeltas.filter((item) => item.done).map((item) => item.delta)
|
|
1581
|
+
expect(doneBubbles).toContain('好的,我来创建。')
|
|
1582
|
+
expect(result.bubbles).toEqual(expect.arrayContaining(['好的,我来创建。']))
|
|
1583
|
+
// Final array keeps the full turn projection (streamed prefix stable).
|
|
1584
|
+
expect(result.bubbles.slice(0, doneBubbles.length)).toEqual(doneBubbles)
|
|
1585
|
+
expect(result.bubbles).not.toEqual([MISSED_TOOL_FAILURE_BUBBLE])
|
|
1586
|
+
// Card tool: English tool confirm stays out of bubbles.
|
|
1587
|
+
expect(result.bubbles.join('')).not.toContain('Created assistant')
|
|
1588
|
+
})
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1364
1591
|
describe('read-only worker notifications', () => {
|
|
1365
1592
|
it('passes prior user constraints and exact card data to completions without any mutation tool', async () => {
|
|
1366
1593
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-readonly-'))
|
|
@@ -20,6 +20,7 @@ export function isPiCodingToolName(name) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function resolveAssistantCwd(assistant) {
|
|
23
|
+
if (assistant?.workspacePath === null) return os.homedir();
|
|
23
24
|
for (const value of [assistant?.effectiveWorkspacePath, assistant?.workspacePath]) {
|
|
24
25
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
25
26
|
}
|
|
@@ -113,6 +114,7 @@ export function formatPiCodingPrompt({ cwd, skillsPrompt = '', tools = [] } = {}
|
|
|
113
114
|
return [
|
|
114
115
|
`Working directory: ${cwd}`,
|
|
115
116
|
'You have application-owned pi coding tools in this directory: read, write, edit, bash.',
|
|
117
|
+
'Use this directory as the starting point for workspace discovery and small, bounded tasks. Substantial implementation belongs in a worker session via assign_session; keep this contact focused on conversation, coordination, and reporting results.',
|
|
116
118
|
'These are not OpenCode native tools and not MCP. Skill directory entries below are instructions only — load a matching skill by calling read on its path; never invent skill or MCP tool names (no context7, gh_grep, webfetch, etc.).',
|
|
117
119
|
'This is your working directory. Ignore any other cwd from the environment, including temporary generator workspaces under /var/folders or os.tmpdir.',
|
|
118
120
|
'When the user asks pwd, the current directory, or to look at files, call bash/read yourself. Never say you have no terminal or cannot read or write files.',
|
|
@@ -30,6 +30,12 @@ afterEach(() => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
describe('resolveAssistantCwd', () => {
|
|
33
|
+
it('starts a managed contact in the server user home while preserving its binding metadata', () => {
|
|
34
|
+
const assistant = { workspacePath: null, effectiveWorkspacePath: '/managed/contact' };
|
|
35
|
+
expect(resolveAssistantCwd(assistant)).toBe(os.homedir());
|
|
36
|
+
expect(assistant.effectiveWorkspacePath).toBe('/managed/contact');
|
|
37
|
+
});
|
|
38
|
+
|
|
33
39
|
it('prefers effectiveWorkspacePath over workspacePath', () => {
|
|
34
40
|
expect(resolveAssistantCwd({
|
|
35
41
|
effectiveWorkspacePath: '/effective',
|
|
@@ -99,6 +105,8 @@ describe('formatPiCodingPrompt', () => {
|
|
|
99
105
|
expect(prompt).toContain('not OpenCode native tools and not MCP');
|
|
100
106
|
expect(prompt).toContain('never invent skill or MCP tool names');
|
|
101
107
|
expect(prompt).toContain('context7');
|
|
108
|
+
expect(prompt).toContain('small, bounded tasks');
|
|
109
|
+
expect(prompt).toContain('worker session via assign_session');
|
|
102
110
|
await runtime.close();
|
|
103
111
|
});
|
|
104
112
|
});
|