@pikku/core 0.12.2 → 0.12.3
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/CHANGELOG.md +18 -0
- package/dist/crypto-utils.d.ts +10 -0
- package/dist/crypto-utils.js +62 -0
- package/dist/function/function-runner.js +4 -4
- package/dist/function/functions.types.d.ts +26 -1
- package/dist/function/functions.types.js +16 -0
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +7 -3
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/pikku-state.js +3 -1
- package/dist/schema.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +8 -2
- package/dist/services/in-memory-workflow-service.js +28 -1
- package/dist/services/workflow-service.d.ts +8 -2
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/service-tests.d.ts +31 -0
- package/dist/testing/service-tests.js +598 -0
- package/dist/types/core.types.d.ts +9 -1
- package/dist/types/state.types.d.ts +4 -2
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
- package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
- package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -2
- package/dist/wirings/ai-agent/index.js +1 -2
- package/dist/wirings/channel/channel-handler.js +5 -1
- package/dist/wirings/channel/channel.types.d.ts +1 -7
- package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -3
- package/dist/wirings/channel/local/local-channel-runner.js +14 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
- package/dist/wirings/cli/cli-runner.js +10 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -0
- package/dist/wirings/http/http-routes.js +0 -1
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +25 -3
- package/dist/wirings/http/http.types.d.ts +3 -11
- package/dist/wirings/http/index.d.ts +2 -1
- package/dist/wirings/http/index.js +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
- package/dist/wirings/http/web-request.d.ts +12 -0
- package/dist/wirings/http/web-request.js +85 -0
- package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +13 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
- package/dist/wirings/rpc/rpc-runner.js +36 -14
- package/dist/wirings/rpc/rpc-types.d.ts +6 -1
- package/dist/wirings/rpc/wire-addon.d.ts +1 -0
- package/dist/wirings/rpc/wire-addon.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -0
- package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
- package/dist/wirings/workflow/graph/graph-validation.js +175 -0
- package/dist/wirings/workflow/graph/index.d.ts +1 -0
- package/dist/wirings/workflow/graph/index.js +1 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
- package/dist/wirings/workflow/workflow.types.d.ts +10 -1
- package/package.json +4 -2
- package/src/crypto-utils.test.ts +99 -1
- package/src/crypto-utils.ts +114 -0
- package/src/function/function-runner.ts +5 -5
- package/src/function/functions.types.ts +38 -1
- package/src/handle-error.test.ts +65 -2
- package/src/handle-error.ts +8 -4
- package/src/index.ts +14 -0
- package/src/pikku-state.ts +3 -1
- package/src/schema.ts +1 -1
- package/src/services/in-memory-workflow-service.ts +45 -2
- package/src/services/workflow-service.ts +12 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/types/core.types.ts +9 -1
- package/src/types/state.types.ts +4 -2
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +7 -6
- package/src/wirings/ai-agent/ai-agent-prepare.ts +175 -61
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +182 -89
- package/src/wirings/ai-agent/ai-agent.types.ts +23 -1
- package/src/wirings/ai-agent/index.ts +1 -5
- package/src/wirings/channel/channel-handler.ts +11 -2
- package/src/wirings/channel/channel.types.ts +1 -7
- package/src/wirings/channel/local/local-channel-handler.ts +5 -3
- package/src/wirings/channel/local/local-channel-runner.ts +17 -7
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +12 -2
- package/src/wirings/cli/cli.types.ts +1 -0
- package/src/wirings/http/http-routes.ts +0 -1
- package/src/wirings/http/http-runner.ts +35 -10
- package/src/wirings/http/http.types.ts +3 -12
- package/src/wirings/http/index.ts +3 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +30 -12
- package/src/wirings/rpc/rpc-runner.ts +45 -24
- package/src/wirings/rpc/rpc-types.ts +6 -1
- package/src/wirings/rpc/wire-addon.ts +2 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +5 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
- package/src/wirings/workflow/workflow.types.ts +9 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
- package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
- package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
export function createAssistantUIChannel(parent) {
|
|
3
|
-
const messageId = randomUUID();
|
|
4
|
-
let started = false;
|
|
5
|
-
let totalInputTokens = 0;
|
|
6
|
-
let totalOutputTokens = 0;
|
|
7
|
-
let pendingToolResults = [];
|
|
8
|
-
let hasToolCalls = false;
|
|
9
|
-
let hasPendingApproval = false;
|
|
10
|
-
const sendChunk = (chunk) => {
|
|
11
|
-
parent.send(chunk);
|
|
12
|
-
};
|
|
13
|
-
const ensureStarted = () => {
|
|
14
|
-
if (!started) {
|
|
15
|
-
started = true;
|
|
16
|
-
sendChunk({ type: 'start', messageId });
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
const flushToolResults = () => {
|
|
20
|
-
for (const chunk of pendingToolResults) {
|
|
21
|
-
sendChunk(chunk);
|
|
22
|
-
}
|
|
23
|
-
pendingToolResults = [];
|
|
24
|
-
};
|
|
25
|
-
return {
|
|
26
|
-
channelId: parent.channelId,
|
|
27
|
-
openingData: parent.openingData,
|
|
28
|
-
get state() {
|
|
29
|
-
return parent.state;
|
|
30
|
-
},
|
|
31
|
-
close: () => parent.close(),
|
|
32
|
-
sendBinary: (data) => parent.sendBinary(data),
|
|
33
|
-
send: (event) => {
|
|
34
|
-
switch (event.type) {
|
|
35
|
-
case 'text-delta':
|
|
36
|
-
ensureStarted();
|
|
37
|
-
sendChunk({ type: 'text-delta', textDelta: event.text });
|
|
38
|
-
break;
|
|
39
|
-
case 'reasoning-delta':
|
|
40
|
-
ensureStarted();
|
|
41
|
-
sendChunk({ type: 'reasoning-delta', delta: event.text });
|
|
42
|
-
break;
|
|
43
|
-
case 'audio-delta':
|
|
44
|
-
ensureStarted();
|
|
45
|
-
sendChunk({ type: 'audio-delta', data: event });
|
|
46
|
-
break;
|
|
47
|
-
case 'audio-done':
|
|
48
|
-
ensureStarted();
|
|
49
|
-
sendChunk({ type: 'audio-done', data: event });
|
|
50
|
-
break;
|
|
51
|
-
case 'tool-call':
|
|
52
|
-
ensureStarted();
|
|
53
|
-
hasToolCalls = true;
|
|
54
|
-
sendChunk({
|
|
55
|
-
type: 'tool-call-start',
|
|
56
|
-
id: event.toolCallId,
|
|
57
|
-
toolCallId: event.toolCallId,
|
|
58
|
-
toolName: event.toolName,
|
|
59
|
-
});
|
|
60
|
-
sendChunk({
|
|
61
|
-
type: 'tool-call-delta',
|
|
62
|
-
argsText: typeof event.args === 'string'
|
|
63
|
-
? event.args
|
|
64
|
-
: JSON.stringify(event.args),
|
|
65
|
-
});
|
|
66
|
-
sendChunk({ type: 'tool-call-end' });
|
|
67
|
-
break;
|
|
68
|
-
case 'tool-result':
|
|
69
|
-
ensureStarted();
|
|
70
|
-
pendingToolResults.push({
|
|
71
|
-
type: 'tool-result',
|
|
72
|
-
toolCallId: event.toolCallId,
|
|
73
|
-
result: event.result,
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
76
|
-
case 'agent-call':
|
|
77
|
-
case 'agent-result':
|
|
78
|
-
break;
|
|
79
|
-
case 'usage': {
|
|
80
|
-
ensureStarted();
|
|
81
|
-
totalInputTokens += event.tokens.input;
|
|
82
|
-
totalOutputTokens += event.tokens.output;
|
|
83
|
-
const hasPending = pendingToolResults.length > 0;
|
|
84
|
-
sendChunk({
|
|
85
|
-
type: 'finish-step',
|
|
86
|
-
finishReason: hasToolCalls ? 'tool-calls' : 'stop',
|
|
87
|
-
usage: {
|
|
88
|
-
promptTokens: event.tokens.input,
|
|
89
|
-
completionTokens: event.tokens.output,
|
|
90
|
-
},
|
|
91
|
-
isContinued: hasPending,
|
|
92
|
-
});
|
|
93
|
-
flushToolResults();
|
|
94
|
-
hasToolCalls = false;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
case 'error':
|
|
98
|
-
ensureStarted();
|
|
99
|
-
sendChunk({ type: 'error', errorText: event.message });
|
|
100
|
-
break;
|
|
101
|
-
case 'approval-request':
|
|
102
|
-
ensureStarted();
|
|
103
|
-
hasPendingApproval = true;
|
|
104
|
-
sendChunk({ type: 'data-approval-request', data: event });
|
|
105
|
-
break;
|
|
106
|
-
case 'suspended':
|
|
107
|
-
ensureStarted();
|
|
108
|
-
sendChunk({ type: 'data-suspended', data: event });
|
|
109
|
-
break;
|
|
110
|
-
case 'done':
|
|
111
|
-
ensureStarted();
|
|
112
|
-
flushToolResults();
|
|
113
|
-
sendChunk({
|
|
114
|
-
type: 'finish',
|
|
115
|
-
finishReason: hasPendingApproval ? 'tool-calls' : 'stop',
|
|
116
|
-
usage: {
|
|
117
|
-
promptTokens: totalInputTokens,
|
|
118
|
-
completionTokens: totalOutputTokens,
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
parent.send('[DONE]');
|
|
122
|
-
parent.close();
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
export function parseAssistantUIInput(input, defaults) {
|
|
129
|
-
const messages = input.messages;
|
|
130
|
-
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
131
|
-
throw new Error('assistant-ui input must contain a non-empty messages array');
|
|
132
|
-
}
|
|
133
|
-
const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user');
|
|
134
|
-
if (!lastUserMessage) {
|
|
135
|
-
throw new Error('No user message found in assistant-ui input');
|
|
136
|
-
}
|
|
137
|
-
let messageText;
|
|
138
|
-
let attachments;
|
|
139
|
-
const extractFromParts = (parts) => {
|
|
140
|
-
const textParts = parts.filter((p) => p.type === 'text');
|
|
141
|
-
messageText = textParts.map((p) => String(p.text)).join(' ') || '';
|
|
142
|
-
const nonTextParts = parts.filter((p) => p.type === 'image' || p.type === 'file');
|
|
143
|
-
if (nonTextParts.length > 0) {
|
|
144
|
-
attachments = nonTextParts.map((p) => ({
|
|
145
|
-
type: p.type,
|
|
146
|
-
data: p.data,
|
|
147
|
-
url: p.url,
|
|
148
|
-
mediaType: p.mediaType,
|
|
149
|
-
filename: p.filename,
|
|
150
|
-
}));
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
if (typeof lastUserMessage.content === 'string') {
|
|
154
|
-
messageText = lastUserMessage.content;
|
|
155
|
-
}
|
|
156
|
-
else if (Array.isArray(lastUserMessage.parts)) {
|
|
157
|
-
extractFromParts(lastUserMessage.parts);
|
|
158
|
-
}
|
|
159
|
-
else if (Array.isArray(lastUserMessage.content)) {
|
|
160
|
-
extractFromParts(lastUserMessage.content);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
messageText = '';
|
|
164
|
-
}
|
|
165
|
-
const threadId = input.threadId ?? input.id ?? randomUUID();
|
|
166
|
-
const resourceId = defaults?.resourceId ?? 'default';
|
|
167
|
-
return { message: messageText, threadId, resourceId, attachments };
|
|
168
|
-
}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
createAssistantUIChannel,
|
|
6
|
-
parseAssistantUIInput,
|
|
7
|
-
} from './ai-agent-assistant-ui.js'
|
|
8
|
-
import type { AIStreamChannel, AIStreamEvent } from './ai-agent.types.js'
|
|
9
|
-
|
|
10
|
-
function createMockChannel(): AIStreamChannel & { sent: unknown[] } {
|
|
11
|
-
const sent: unknown[] = []
|
|
12
|
-
return {
|
|
13
|
-
channelId: 'test-channel',
|
|
14
|
-
openingData: undefined,
|
|
15
|
-
state: 'open' as const,
|
|
16
|
-
sent,
|
|
17
|
-
send: (data: unknown) => {
|
|
18
|
-
sent.push(data)
|
|
19
|
-
},
|
|
20
|
-
close: () => {},
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('createAssistantUIChannel', () => {
|
|
25
|
-
test('sends start event before first content', () => {
|
|
26
|
-
const parent = createMockChannel()
|
|
27
|
-
const channel = createAssistantUIChannel(parent)
|
|
28
|
-
|
|
29
|
-
channel.send({ type: 'text-delta', text: 'Hello' })
|
|
30
|
-
|
|
31
|
-
assert.equal(parent.sent.length, 2)
|
|
32
|
-
const start = parent.sent[0] as any
|
|
33
|
-
assert.equal(start.type, 'start')
|
|
34
|
-
assert.ok(start.messageId)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('sends start only once', () => {
|
|
38
|
-
const parent = createMockChannel()
|
|
39
|
-
const channel = createAssistantUIChannel(parent)
|
|
40
|
-
|
|
41
|
-
channel.send({ type: 'text-delta', text: 'Hello' })
|
|
42
|
-
channel.send({ type: 'text-delta', text: ' world' })
|
|
43
|
-
|
|
44
|
-
const startEvents = parent.sent.filter((e: any) => e.type === 'start')
|
|
45
|
-
assert.equal(startEvents.length, 1)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('maps text-delta correctly', () => {
|
|
49
|
-
const parent = createMockChannel()
|
|
50
|
-
const channel = createAssistantUIChannel(parent)
|
|
51
|
-
|
|
52
|
-
channel.send({ type: 'text-delta', text: 'Hello' })
|
|
53
|
-
|
|
54
|
-
const delta = parent.sent[1] as any
|
|
55
|
-
assert.equal(delta.type, 'text-delta')
|
|
56
|
-
assert.equal(delta.textDelta, 'Hello')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('maps reasoning-delta correctly', () => {
|
|
60
|
-
const parent = createMockChannel()
|
|
61
|
-
const channel = createAssistantUIChannel(parent)
|
|
62
|
-
|
|
63
|
-
channel.send({ type: 'reasoning-delta', text: 'thinking...' })
|
|
64
|
-
|
|
65
|
-
const delta = parent.sent[1] as any
|
|
66
|
-
assert.equal(delta.type, 'reasoning-delta')
|
|
67
|
-
assert.equal(delta.delta, 'thinking...')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('expands tool-call into start/delta/end', () => {
|
|
71
|
-
const parent = createMockChannel()
|
|
72
|
-
const channel = createAssistantUIChannel(parent)
|
|
73
|
-
|
|
74
|
-
channel.send({
|
|
75
|
-
type: 'tool-call',
|
|
76
|
-
toolCallId: 'tc-1',
|
|
77
|
-
toolName: 'search',
|
|
78
|
-
args: { query: 'hello' },
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
assert.equal(parent.sent.length, 4)
|
|
82
|
-
const start = parent.sent[1] as any
|
|
83
|
-
assert.equal(start.type, 'tool-call-start')
|
|
84
|
-
assert.equal(start.id, 'tc-1')
|
|
85
|
-
assert.equal(start.toolCallId, 'tc-1')
|
|
86
|
-
assert.equal(start.toolName, 'search')
|
|
87
|
-
|
|
88
|
-
const delta = parent.sent[2] as any
|
|
89
|
-
assert.equal(delta.type, 'tool-call-delta')
|
|
90
|
-
assert.equal(delta.argsText, '{"query":"hello"}')
|
|
91
|
-
|
|
92
|
-
const end = parent.sent[3] as any
|
|
93
|
-
assert.equal(end.type, 'tool-call-end')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('buffers tool-result and flushes after finish-step', () => {
|
|
97
|
-
const parent = createMockChannel()
|
|
98
|
-
const channel = createAssistantUIChannel(parent)
|
|
99
|
-
|
|
100
|
-
channel.send({
|
|
101
|
-
type: 'tool-call',
|
|
102
|
-
toolCallId: 'tc-1',
|
|
103
|
-
toolName: 'search',
|
|
104
|
-
args: { query: 'hello' },
|
|
105
|
-
})
|
|
106
|
-
channel.send({
|
|
107
|
-
type: 'tool-result',
|
|
108
|
-
toolCallId: 'tc-1',
|
|
109
|
-
toolName: 'search',
|
|
110
|
-
result: { data: [1, 2, 3] },
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
const beforeUsage = parent.sent.filter((e: any) => e.type === 'tool-result')
|
|
114
|
-
assert.equal(beforeUsage.length, 0)
|
|
115
|
-
|
|
116
|
-
channel.send({
|
|
117
|
-
type: 'usage',
|
|
118
|
-
tokens: { input: 100, output: 50 },
|
|
119
|
-
model: 'gpt-4o',
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const afterUsage = parent.sent.filter((e: any) => e.type === 'tool-result')
|
|
123
|
-
assert.equal(afterUsage.length, 1)
|
|
124
|
-
assert.equal(afterUsage[0].toolCallId, 'tc-1')
|
|
125
|
-
assert.deepEqual(afterUsage[0].result, { data: [1, 2, 3] })
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('silently ignores agent-call (covered by tool-call)', () => {
|
|
129
|
-
const parent = createMockChannel()
|
|
130
|
-
const channel = createAssistantUIChannel(parent)
|
|
131
|
-
|
|
132
|
-
channel.send({
|
|
133
|
-
type: 'agent-call',
|
|
134
|
-
agentName: 'sub-agent',
|
|
135
|
-
session: 'sess-1',
|
|
136
|
-
input: { message: 'do something' },
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
assert.equal(parent.sent.length, 0)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test('silently ignores agent-result (covered by tool-result)', () => {
|
|
143
|
-
const parent = createMockChannel()
|
|
144
|
-
const channel = createAssistantUIChannel(parent)
|
|
145
|
-
|
|
146
|
-
channel.send({
|
|
147
|
-
type: 'agent-result',
|
|
148
|
-
agentName: 'sub-agent',
|
|
149
|
-
session: 'sess-1',
|
|
150
|
-
result: 'done!',
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
assert.equal(parent.sent.length, 0)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('maps usage to finish-step with token counts', () => {
|
|
157
|
-
const parent = createMockChannel()
|
|
158
|
-
const channel = createAssistantUIChannel(parent)
|
|
159
|
-
|
|
160
|
-
channel.send({
|
|
161
|
-
type: 'usage',
|
|
162
|
-
tokens: { input: 100, output: 50 },
|
|
163
|
-
model: 'gpt-4o',
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
const step = parent.sent[1] as any
|
|
167
|
-
assert.equal(step.type, 'finish-step')
|
|
168
|
-
assert.equal(step.finishReason, 'stop')
|
|
169
|
-
assert.deepEqual(step.usage, { promptTokens: 100, completionTokens: 50 })
|
|
170
|
-
assert.equal(step.isContinued, false)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
test('maps error correctly', () => {
|
|
174
|
-
const parent = createMockChannel()
|
|
175
|
-
const channel = createAssistantUIChannel(parent)
|
|
176
|
-
|
|
177
|
-
channel.send({ type: 'error', message: 'something went wrong' })
|
|
178
|
-
|
|
179
|
-
const err = parent.sent[1] as any
|
|
180
|
-
assert.equal(err.type, 'error')
|
|
181
|
-
assert.equal(err.errorText, 'something went wrong')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('maps approval-request to data event', () => {
|
|
185
|
-
const parent = createMockChannel()
|
|
186
|
-
const channel = createAssistantUIChannel(parent)
|
|
187
|
-
|
|
188
|
-
const event: AIStreamEvent = {
|
|
189
|
-
type: 'approval-request',
|
|
190
|
-
toolCallId: 'tc-1',
|
|
191
|
-
toolName: 'deleteTodo',
|
|
192
|
-
args: { id: 5 },
|
|
193
|
-
}
|
|
194
|
-
channel.send(event)
|
|
195
|
-
|
|
196
|
-
const chunk = parent.sent[1] as any
|
|
197
|
-
assert.equal(chunk.type, 'data-approval-request')
|
|
198
|
-
assert.deepEqual(chunk.data, event)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test('maps suspended to data event', () => {
|
|
202
|
-
const parent = createMockChannel()
|
|
203
|
-
const channel = createAssistantUIChannel(parent)
|
|
204
|
-
|
|
205
|
-
const event: AIStreamEvent = {
|
|
206
|
-
type: 'suspended',
|
|
207
|
-
reason: 'rpc-missing',
|
|
208
|
-
missingRpcs: ['listTodos'],
|
|
209
|
-
}
|
|
210
|
-
channel.send(event)
|
|
211
|
-
|
|
212
|
-
const chunk = parent.sent[1] as any
|
|
213
|
-
assert.equal(chunk.type, 'data-suspended')
|
|
214
|
-
assert.deepEqual(chunk.data, event)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
test('done produces finish + raw [DONE] string + calls close', () => {
|
|
218
|
-
const parent = createMockChannel()
|
|
219
|
-
let closed = false
|
|
220
|
-
parent.close = () => {
|
|
221
|
-
closed = true
|
|
222
|
-
}
|
|
223
|
-
const channel = createAssistantUIChannel(parent)
|
|
224
|
-
|
|
225
|
-
channel.send({ type: 'text-delta', text: 'hi' })
|
|
226
|
-
channel.send({ type: 'done' })
|
|
227
|
-
|
|
228
|
-
const finish = parent.sent[2] as any
|
|
229
|
-
assert.equal(finish.type, 'finish')
|
|
230
|
-
assert.equal(finish.finishReason, 'stop')
|
|
231
|
-
|
|
232
|
-
assert.equal(parent.sent[3], '[DONE]')
|
|
233
|
-
assert.ok(closed)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
test('accumulates usage across multiple steps', () => {
|
|
237
|
-
const parent = createMockChannel()
|
|
238
|
-
const channel = createAssistantUIChannel(parent)
|
|
239
|
-
|
|
240
|
-
channel.send({
|
|
241
|
-
type: 'usage',
|
|
242
|
-
tokens: { input: 100, output: 50 },
|
|
243
|
-
model: 'gpt-4o',
|
|
244
|
-
})
|
|
245
|
-
channel.send({
|
|
246
|
-
type: 'usage',
|
|
247
|
-
tokens: { input: 200, output: 100 },
|
|
248
|
-
model: 'gpt-4o',
|
|
249
|
-
})
|
|
250
|
-
channel.send({ type: 'done' })
|
|
251
|
-
|
|
252
|
-
const finish = parent.sent.find((e: any) => e.type === 'finish') as any
|
|
253
|
-
assert.deepEqual(finish.usage, {
|
|
254
|
-
promptTokens: 300,
|
|
255
|
-
completionTokens: 150,
|
|
256
|
-
})
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
test('preserves parent channelId and openingData', () => {
|
|
260
|
-
const parent = createMockChannel()
|
|
261
|
-
parent.channelId = 'my-channel'
|
|
262
|
-
parent.openingData = { foo: 'bar' }
|
|
263
|
-
|
|
264
|
-
const channel = createAssistantUIChannel(parent)
|
|
265
|
-
|
|
266
|
-
assert.equal(channel.channelId, 'my-channel')
|
|
267
|
-
assert.deepEqual(channel.openingData, { foo: 'bar' })
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
describe('parseAssistantUIInput', () => {
|
|
272
|
-
test('extracts last user message with string content', () => {
|
|
273
|
-
const result = parseAssistantUIInput({
|
|
274
|
-
messages: [
|
|
275
|
-
{ role: 'user', content: 'first message' },
|
|
276
|
-
{ role: 'assistant', content: 'response' },
|
|
277
|
-
{ role: 'user', content: 'second message' },
|
|
278
|
-
],
|
|
279
|
-
threadId: 'thread-abc',
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
assert.equal(result.message, 'second message')
|
|
283
|
-
assert.equal(result.threadId, 'thread-abc')
|
|
284
|
-
assert.equal(result.resourceId, 'default')
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
test('handles parts format (UIMessage)', () => {
|
|
288
|
-
const result = parseAssistantUIInput({
|
|
289
|
-
messages: [
|
|
290
|
-
{
|
|
291
|
-
role: 'user',
|
|
292
|
-
parts: [{ type: 'text', text: 'hello from parts' }],
|
|
293
|
-
},
|
|
294
|
-
],
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
assert.equal(result.message, 'hello from parts')
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
test('handles content array format', () => {
|
|
301
|
-
const result = parseAssistantUIInput({
|
|
302
|
-
messages: [
|
|
303
|
-
{
|
|
304
|
-
role: 'user',
|
|
305
|
-
content: [{ type: 'text', text: 'hello from content array' }],
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
assert.equal(result.message, 'hello from content array')
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
test('uses input.id as threadId fallback', () => {
|
|
314
|
-
const result = parseAssistantUIInput({
|
|
315
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
316
|
-
id: 'id-fallback',
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
assert.equal(result.threadId, 'id-fallback')
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
test('generates UUID when no threadId or id', () => {
|
|
323
|
-
const result = parseAssistantUIInput({
|
|
324
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
assert.ok(result.threadId)
|
|
328
|
-
assert.match(
|
|
329
|
-
result.threadId,
|
|
330
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
331
|
-
)
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
test('uses defaults.resourceId when provided', () => {
|
|
335
|
-
const result = parseAssistantUIInput(
|
|
336
|
-
{ messages: [{ role: 'user', content: 'hi' }] },
|
|
337
|
-
{ resourceId: 'my-resource' }
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
assert.equal(result.resourceId, 'my-resource')
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
test('throws on empty messages array', () => {
|
|
344
|
-
assert.throws(
|
|
345
|
-
() => parseAssistantUIInput({ messages: [] }),
|
|
346
|
-
/non-empty messages array/
|
|
347
|
-
)
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
test('throws on missing messages', () => {
|
|
351
|
-
assert.throws(() => parseAssistantUIInput({}), /non-empty messages array/)
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
test('throws when no user message found', () => {
|
|
355
|
-
assert.throws(
|
|
356
|
-
() =>
|
|
357
|
-
parseAssistantUIInput({
|
|
358
|
-
messages: [{ role: 'assistant', content: 'no user here' }],
|
|
359
|
-
}),
|
|
360
|
-
/No user message found/
|
|
361
|
-
)
|
|
362
|
-
})
|
|
363
|
-
})
|