experimental-a2 0.18.1 → 0.19.0
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 +28 -0
- package/dist/{ai-BMLlI2Ff.d.ts → ai-Cz3hqVhk.d.ts} +33 -16
- package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +1 -1
- package/dist/ai-control-CvLbW10B.js +46 -0
- package/dist/ai-control-CvLbW10B.js.map +1 -0
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +30 -10
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
- package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
- package/dist/{ai-harness-B2suP2Qh.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
- package/dist/{ai-D6mGlO8j.js → ai-mMiWul1T.js} +116 -77
- package/dist/ai-mMiWul1T.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.d.ts.map +1 -1
- package/dist/ai-pi.js +55 -10
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -1
- package/dist/ai-server.d.ts +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +118 -50
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +1 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/docs/agents/03-conversations.mdx +25 -13
- package/docs/agents/04-harnesses.mdx +51 -5
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/reference/04-ai.mdx +27 -18
- package/docs/reference/05-adapters.mdx +2 -2
- package/docs/server/02-timers.mdx +1 -1
- package/docs/server/05-production.mdx +2 -2
- package/examples/playground/DEPLOY.md +9 -0
- package/examples/playground/app/a0/a0.css +244 -5
- package/examples/playground/app/a0/app-view.tsx +2 -1
- package/examples/playground/app/a0/attachments.ts +133 -0
- package/examples/playground/app/a0/chat.test.tsx +504 -0
- package/examples/playground/app/a0/chat.tsx +623 -63
- package/examples/playground/app/a0/context-menu.tsx +38 -11
- package/examples/playground/app/a0/model.test.ts +28 -0
- package/examples/playground/app/a0/model.ts +30 -0
- package/examples/playground/app/a0/server.ts +5 -0
- package/examples/playground/app/a0/uploads/route.ts +29 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
- package/examples/playground/app/agent/model.test.ts +12 -3
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
- package/examples/playground/next.config.ts +7 -4
- package/examples/playground/package.json +3 -1
- package/package.json +1 -1
- package/src/ai-agent.ts +4 -0
- package/src/ai-client-state.ts +21 -9
- package/src/ai-context-schema.ts +28 -4
- package/src/ai-context.ts +83 -3
- package/src/ai-control-server.ts +70 -43
- package/src/ai-control-state.ts +3 -4
- package/src/ai-control.ts +59 -10
- package/src/ai-coordinator.ts +7 -6
- package/src/ai-fx.ts +58 -20
- package/src/ai-harness.ts +153 -29
- package/src/ai-message-shape.ts +41 -0
- package/src/ai-pi.ts +88 -32
- package/src/ai-stored-state.ts +1 -3
- package/src/ai.ts +116 -77
- package/src/libfx.d.ts +8 -1
- package/src/scheduler-vercel.ts +1 -12
- package/dist/ai-BMLlI2Ff.d.ts.map +0 -1
- package/dist/ai-D6mGlO8j.js.map +0 -1
- package/dist/ai-harness-B2suP2Qh.d.ts.map +0 -1
- package/dist/ai-harness-BuLh_EYi.js.map +0 -1
- package/examples/playground/app/a0/no-photon.cjs +0 -4
package/src/ai-harness.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import {
|
|
2
|
+
createDownload,
|
|
3
|
+
type Experimental_SandboxSession,
|
|
4
|
+
type FileUIPart,
|
|
5
|
+
type FinishReason,
|
|
6
|
+
type LanguageModelUsage,
|
|
7
|
+
type ToolSet,
|
|
8
|
+
type UIMessage,
|
|
9
|
+
type UIMessageChunk,
|
|
8
10
|
} from 'ai'
|
|
9
11
|
import type {
|
|
10
12
|
GenerationCompletedPayload,
|
|
13
|
+
MessageCreatedPayload,
|
|
11
14
|
ReasoningLevel,
|
|
12
15
|
ToolResultPayload,
|
|
13
16
|
} from './ai.ts'
|
|
17
|
+
import { STEERED_REASON } from './ai-control.ts'
|
|
14
18
|
import { boundedCache, type BoundedCache } from './bounded-cache.ts'
|
|
15
19
|
import type { ContractEvent, EventDefs } from './contract.ts'
|
|
16
20
|
|
|
@@ -307,21 +311,21 @@ function savedEnvelope(value: unknown): SavedEnvelope | undefined {
|
|
|
307
311
|
|
|
308
312
|
// ------------------------------------------------- the log as harness turns
|
|
309
313
|
|
|
310
|
-
// A2's reason on `ai.message.interrupted` when a steer ends a response.
|
|
311
|
-
const STEERED = 'steered after step'
|
|
312
|
-
|
|
313
314
|
/** A completed generation as the log recorded it. */
|
|
314
315
|
export type RecordedGeneration = {
|
|
315
316
|
chunks: UIMessageChunk[]
|
|
316
317
|
completed: GenerationCompletedPayload
|
|
317
318
|
}
|
|
318
319
|
|
|
320
|
+
/** What user messages hand a harness: their text, and the files they attach. */
|
|
321
|
+
export type HarnessInput = { text: string; files: FileUIPart[] }
|
|
322
|
+
|
|
319
323
|
/** One harness turn to rebuild: its prompt, completed generations, and end. */
|
|
320
324
|
export type HarnessTurn = {
|
|
321
|
-
prompt:
|
|
325
|
+
prompt: HarnessInput
|
|
322
326
|
generations: RecordedGeneration[]
|
|
323
327
|
/** Steering that entered after the generation at that index. */
|
|
324
|
-
steers: Map<number,
|
|
328
|
+
steers: Map<number, HarnessInput[]>
|
|
325
329
|
end: 'stop' | 'interrupted' | 'live'
|
|
326
330
|
}
|
|
327
331
|
|
|
@@ -342,15 +346,18 @@ export function harnessTurns(
|
|
|
342
346
|
}
|
|
343
347
|
type Response = {
|
|
344
348
|
id: string
|
|
345
|
-
prompt:
|
|
349
|
+
prompt: HarnessInput[]
|
|
346
350
|
generations: Generation[]
|
|
347
351
|
end?: 'stop' | 'steered' | 'interrupted'
|
|
348
352
|
}
|
|
349
|
-
//
|
|
350
|
-
|
|
353
|
+
// Prompts take their content from the transcript: a harness agent never
|
|
354
|
+
// runs A2's compaction, so the step's messages hold every input admitted by
|
|
355
|
+
// its read, before the snapshot or after it. A session another loop
|
|
356
|
+
// compacted can't move to a harness.
|
|
357
|
+
const userInput = new Map<string, HarnessInput>(
|
|
351
358
|
step.messages
|
|
352
359
|
.filter((message) => message.role === 'user')
|
|
353
|
-
.map((message) => [message.id,
|
|
360
|
+
.map((message) => [message.id, inputOf(message)]),
|
|
354
361
|
)
|
|
355
362
|
let passive: string[] = []
|
|
356
363
|
const responses = new Map<string, Response>()
|
|
@@ -362,10 +369,9 @@ export function harnessTurns(
|
|
|
362
369
|
const payload = event.payload as Record<string, unknown>
|
|
363
370
|
switch (event.type) {
|
|
364
371
|
case 'ai.message.created': {
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
if (payload['generate'] === false) passive.push(message.id)
|
|
372
|
+
const admitted = event.payload as MessageCreatedPayload
|
|
373
|
+
if (admitted.role === 'user' && admitted.generate === false)
|
|
374
|
+
passive.push(admitted.messageId)
|
|
369
375
|
break
|
|
370
376
|
}
|
|
371
377
|
case 'ai.generation.requested': {
|
|
@@ -376,7 +382,7 @@ export function harnessTurns(
|
|
|
376
382
|
passive = []
|
|
377
383
|
responses.set(id, {
|
|
378
384
|
id,
|
|
379
|
-
prompt: ids.flatMap((p) =>
|
|
385
|
+
prompt: ids.flatMap((p) => userInput.get(p) ?? []),
|
|
380
386
|
generations: [],
|
|
381
387
|
})
|
|
382
388
|
break
|
|
@@ -420,7 +426,7 @@ export function harnessTurns(
|
|
|
420
426
|
const response = responses.get(payload['messageId'] as string)
|
|
421
427
|
if (response)
|
|
422
428
|
response.end =
|
|
423
|
-
payload['reason'] ===
|
|
429
|
+
payload['reason'] === STEERED_REASON ? 'steered' : 'interrupted'
|
|
424
430
|
break
|
|
425
431
|
}
|
|
426
432
|
}
|
|
@@ -429,13 +435,16 @@ export function harnessTurns(
|
|
|
429
435
|
const turns: HarnessTurn[] = []
|
|
430
436
|
let steered = false
|
|
431
437
|
for (const response of responses.values()) {
|
|
432
|
-
const
|
|
438
|
+
const input: HarnessInput = {
|
|
439
|
+
text: response.prompt.map((prompt) => prompt.text).join('\n\n'),
|
|
440
|
+
files: response.prompt.flatMap((prompt) => prompt.files),
|
|
441
|
+
}
|
|
433
442
|
let turn = turns.at(-1)
|
|
434
443
|
if (steered && turn !== undefined) {
|
|
435
444
|
const at = Math.max(0, turn.generations.length - 1)
|
|
436
|
-
turn.steers.set(at, [...(turn.steers.get(at) ?? []),
|
|
445
|
+
turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
|
|
437
446
|
} else {
|
|
438
|
-
turn = { prompt:
|
|
447
|
+
turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
|
|
439
448
|
turns.push(turn)
|
|
440
449
|
}
|
|
441
450
|
for (const generation of response.generations)
|
|
@@ -455,10 +464,125 @@ export function harnessTurns(
|
|
|
455
464
|
return { turns, results }
|
|
456
465
|
}
|
|
457
466
|
|
|
458
|
-
function
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
|
|
467
|
+
function inputOf(message: UIMessage): HarnessInput {
|
|
468
|
+
return {
|
|
469
|
+
text: message.parts
|
|
470
|
+
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
471
|
+
.join('\n'),
|
|
472
|
+
files: message.parts.filter(
|
|
473
|
+
(part): part is FileUIPart => part.type === 'file',
|
|
474
|
+
),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ------------------------------------------------------------- attachments
|
|
479
|
+
|
|
480
|
+
/** An attached image as an engine takes it: its bytes in base64. */
|
|
481
|
+
export type HarnessImage = { mediaType: string; data: string }
|
|
482
|
+
|
|
483
|
+
// What one message's files hold in a step's memory, at most.
|
|
484
|
+
const MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024
|
|
485
|
+
|
|
486
|
+
type Attachment = { data: string; bytes: number }
|
|
487
|
+
|
|
488
|
+
// Every step of a response replays its turn, and a URL's file doesn't change,
|
|
489
|
+
// so the process keeps what it fetched.
|
|
490
|
+
const attachments = boundedCache<string, Attachment>({
|
|
491
|
+
maxEntries: 256,
|
|
492
|
+
maxBytes: 64 * 1024 * 1024,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The images an input attaches, as its engine takes them: engines take
|
|
497
|
+
* bytes, not URLs. A file it can't attach fails the step, naming the file,
|
|
498
|
+
* and is never left out: the failure may pass, and the step is retried like
|
|
499
|
+
* any other.
|
|
500
|
+
*/
|
|
501
|
+
export async function harnessImages({
|
|
502
|
+
harness,
|
|
503
|
+
input,
|
|
504
|
+
signal,
|
|
505
|
+
}: {
|
|
506
|
+
/** As the error names it: `fx()` or `pi()`. */
|
|
507
|
+
harness: string
|
|
508
|
+
input: HarnessInput
|
|
509
|
+
signal: AbortSignal
|
|
510
|
+
}): Promise<HarnessImage[]> {
|
|
511
|
+
const images: HarnessImage[] = []
|
|
512
|
+
let budget = MAX_ATTACHMENT_BYTES
|
|
513
|
+
// One at a time, so the budget bounds what the step holds.
|
|
514
|
+
for (const file of input.files) {
|
|
515
|
+
try {
|
|
516
|
+
if (!file.mediaType.startsWith('image/'))
|
|
517
|
+
throw new Error(`it takes images only, not ${file.mediaType}`)
|
|
518
|
+
const attached = await attachment({ url: file.url, budget, signal })
|
|
519
|
+
budget -= attached.bytes
|
|
520
|
+
images.push({
|
|
521
|
+
mediaType: file.mediaType,
|
|
522
|
+
data: attached.data,
|
|
523
|
+
})
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (signal.aborted) throw error
|
|
526
|
+
throw new Error(
|
|
527
|
+
`${harness} couldn't attach ${file.filename ?? 'a file'}: ${
|
|
528
|
+
error instanceof Error ? error.message : String(error)
|
|
529
|
+
}`,
|
|
530
|
+
{ cause: error },
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return images
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function attachment({
|
|
538
|
+
url,
|
|
539
|
+
budget,
|
|
540
|
+
signal,
|
|
541
|
+
}: {
|
|
542
|
+
url: string
|
|
543
|
+
budget: number
|
|
544
|
+
signal: AbortSignal
|
|
545
|
+
}): Promise<Attachment> {
|
|
546
|
+
const tooLarge = () =>
|
|
547
|
+
new Error(
|
|
548
|
+
`a message's files may hold ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MiB in a step`,
|
|
549
|
+
)
|
|
550
|
+
const cached = attachments.get(url)
|
|
551
|
+
if (cached !== undefined) {
|
|
552
|
+
if (cached.bytes > budget) throw tooLarge()
|
|
553
|
+
return cached
|
|
554
|
+
}
|
|
555
|
+
const { protocol } = new URL(url)
|
|
556
|
+
if (protocol !== 'https:' && protocol !== 'data:')
|
|
557
|
+
throw new Error(`attachments must be https: or data: URLs, not ${protocol}`)
|
|
558
|
+
// AI SDK's download checks an https: URL's address and its redirects,
|
|
559
|
+
// which a message's sender chooses; a data: URL holds its bytes.
|
|
560
|
+
const bytes =
|
|
561
|
+
protocol === 'data:'
|
|
562
|
+
? dataBytes(url)
|
|
563
|
+
: (
|
|
564
|
+
await createDownload({ maxBytes: budget })({
|
|
565
|
+
url: new URL(url),
|
|
566
|
+
abortSignal: signal,
|
|
567
|
+
})
|
|
568
|
+
).data
|
|
569
|
+
if (bytes.byteLength > budget) throw tooLarge()
|
|
570
|
+
const attached: Attachment = {
|
|
571
|
+
data: Buffer.from(bytes).toString('base64'),
|
|
572
|
+
bytes: bytes.byteLength,
|
|
573
|
+
}
|
|
574
|
+
if (protocol === 'https:')
|
|
575
|
+
attachments.set(url, attached, attached.data.length)
|
|
576
|
+
return attached
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function dataBytes(url: string): Uint8Array {
|
|
580
|
+
const comma = url.indexOf(',')
|
|
581
|
+
if (comma === -1) throw new Error('its data: URL has no data')
|
|
582
|
+
const body = url.slice(comma + 1)
|
|
583
|
+
return url.slice(0, comma).endsWith(';base64')
|
|
584
|
+
? Buffer.from(body, 'base64')
|
|
585
|
+
: Buffer.from(decodeURIComponent(body))
|
|
462
586
|
}
|
|
463
587
|
|
|
464
588
|
// ------------------------------------------------------------- Gateway
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UIMessage } from 'ai'
|
|
2
|
+
|
|
3
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
5
|
+
|
|
6
|
+
export const isJSONCompatible = (
|
|
7
|
+
value: unknown,
|
|
8
|
+
seen: Set<object> = new Set(),
|
|
9
|
+
): boolean => {
|
|
10
|
+
if (
|
|
11
|
+
value === null ||
|
|
12
|
+
typeof value === 'string' ||
|
|
13
|
+
typeof value === 'boolean'
|
|
14
|
+
) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
18
|
+
if (typeof value === 'undefined') return true
|
|
19
|
+
if (typeof value !== 'object') return false
|
|
20
|
+
if (seen.has(value)) return false
|
|
21
|
+
seen.add(value)
|
|
22
|
+
const valid = Array.isArray(value)
|
|
23
|
+
? value.every((item) => isJSONCompatible(item, seen))
|
|
24
|
+
: Object.getPrototypeOf(value) === Object.prototype &&
|
|
25
|
+
Object.values(value).every((item) => isJSONCompatible(item, seen))
|
|
26
|
+
seen.delete(value)
|
|
27
|
+
return valid
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a value has an AI SDK `UIMessage`'s shape. */
|
|
31
|
+
export const isUIMessage = (value: unknown): value is UIMessage =>
|
|
32
|
+
isRecord(value) &&
|
|
33
|
+
typeof value['id'] === 'string' &&
|
|
34
|
+
(value['role'] === 'user' ||
|
|
35
|
+
value['role'] === 'assistant' ||
|
|
36
|
+
value['role'] === 'system') &&
|
|
37
|
+
Array.isArray(value['parts']) &&
|
|
38
|
+
value['parts'].every(
|
|
39
|
+
(part) => isRecord(part) && typeof part['type'] === 'string',
|
|
40
|
+
) &&
|
|
41
|
+
isJSONCompatible(value)
|
package/src/ai-pi.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
AssistantMessage,
|
|
15
15
|
AssistantMessageEvent,
|
|
16
16
|
AssistantMessageEventStream,
|
|
17
|
+
ImageContent,
|
|
17
18
|
JsonObject,
|
|
18
19
|
TextContent,
|
|
19
20
|
ThinkingContent,
|
|
@@ -50,6 +51,8 @@ import {
|
|
|
50
51
|
gatewayCredential,
|
|
51
52
|
type Harness,
|
|
52
53
|
type HarnessGeneration,
|
|
54
|
+
harnessImages,
|
|
55
|
+
type HarnessInput,
|
|
53
56
|
type HarnessRead,
|
|
54
57
|
type HarnessSandbox,
|
|
55
58
|
type HarnessStep,
|
|
@@ -130,11 +133,14 @@ type PiRecord =
|
|
|
130
133
|
| { call: 'step'; response: StoredResponse }
|
|
131
134
|
| { call: 'compaction'; response: AssistantMessage }
|
|
132
135
|
|
|
136
|
+
/** A user message as Pi takes it. */
|
|
137
|
+
type PiInput = { text: string; images: ImageContent[] }
|
|
138
|
+
|
|
133
139
|
type RecordedCall = {
|
|
134
140
|
kind: 'step' | 'compaction'
|
|
135
141
|
response: AssistantMessage
|
|
136
142
|
/** Steering to queue while serving this call. */
|
|
137
|
-
steers:
|
|
143
|
+
steers: PiInput[]
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
type Saved = {
|
|
@@ -232,7 +238,8 @@ async function runStep(
|
|
|
232
238
|
fail(state, diverged(`call ${state.next} is not a ${kind}`))
|
|
233
239
|
state.next++
|
|
234
240
|
// Steering queued while a response streams enters after its tools.
|
|
235
|
-
for (const text of recorded.steers)
|
|
241
|
+
for (const { text, images } of recorded.steers)
|
|
242
|
+
await session!.steer(text, images.length === 0 ? undefined : images)
|
|
236
243
|
replay(out, recorded.response)
|
|
237
244
|
return
|
|
238
245
|
}
|
|
@@ -252,6 +259,9 @@ async function runStep(
|
|
|
252
259
|
const upstream = realStream(model, context, {
|
|
253
260
|
...options,
|
|
254
261
|
headers: { ...options?.headers, ...gatewayHeaders(credential) },
|
|
262
|
+
...(step.speed === 'fast'
|
|
263
|
+
? { onPayload: fastTier(options?.onPayload) }
|
|
264
|
+
: {}),
|
|
255
265
|
})
|
|
256
266
|
const live = kind === 'step' ? liveStep() : undefined
|
|
257
267
|
for await (const event of upstream) {
|
|
@@ -547,9 +557,10 @@ async function runStep(
|
|
|
547
557
|
}
|
|
548
558
|
for (const plan of read.turns) {
|
|
549
559
|
if (step.signal.aborted) return
|
|
560
|
+
const prompt = await piInput({ input: plan.prompt, signal: step.signal })
|
|
550
561
|
const state: TurnState = {
|
|
551
562
|
plan,
|
|
552
|
-
calls: recordedTurn(plan),
|
|
563
|
+
calls: await recordedTurn({ turn: plan, signal: step.signal }),
|
|
553
564
|
next: 0,
|
|
554
565
|
liveStep: false,
|
|
555
566
|
over: false,
|
|
@@ -557,8 +568,9 @@ async function runStep(
|
|
|
557
568
|
compactions: [],
|
|
558
569
|
}
|
|
559
570
|
turn = state
|
|
560
|
-
const running = session.prompt(
|
|
571
|
+
const running = session.prompt(prompt.text, {
|
|
561
572
|
expandPromptTemplates: false,
|
|
573
|
+
...(prompt.images.length === 0 ? {} : { images: prompt.images }),
|
|
562
574
|
})
|
|
563
575
|
if (plan.end !== 'live') {
|
|
564
576
|
await running.catch((error: unknown) => {
|
|
@@ -622,17 +634,43 @@ async function runStep(
|
|
|
622
634
|
// ------------------------------------------------------------ the plan
|
|
623
635
|
|
|
624
636
|
/** A turn's recorded model calls, in the order Pi made them. */
|
|
625
|
-
function recordedTurn(
|
|
637
|
+
async function recordedTurn({
|
|
638
|
+
turn,
|
|
639
|
+
signal,
|
|
640
|
+
}: {
|
|
641
|
+
turn: HarnessTurn
|
|
642
|
+
signal: AbortSignal
|
|
643
|
+
}): Promise<RecordedCall[]> {
|
|
626
644
|
const calls: RecordedCall[] = []
|
|
627
|
-
turn.generations.
|
|
645
|
+
for (const [index, generation] of turn.generations.entries()) {
|
|
628
646
|
calls.push(...recordedCalls(generation))
|
|
629
647
|
const steers = turn.steers.get(index)
|
|
630
648
|
const last = calls.findLast((call) => call.kind === 'step')
|
|
631
|
-
if (steers !== undefined && last !== undefined)
|
|
632
|
-
|
|
649
|
+
if (steers !== undefined && last !== undefined)
|
|
650
|
+
for (const input of steers)
|
|
651
|
+
last.steers.push(await piInput({ input, signal }))
|
|
652
|
+
}
|
|
633
653
|
return calls
|
|
634
654
|
}
|
|
635
655
|
|
|
656
|
+
async function piInput({
|
|
657
|
+
input,
|
|
658
|
+
signal,
|
|
659
|
+
}: {
|
|
660
|
+
input: HarnessInput
|
|
661
|
+
signal: AbortSignal
|
|
662
|
+
}): Promise<PiInput> {
|
|
663
|
+
const images = await harnessImages({ harness: 'pi()', input, signal })
|
|
664
|
+
return {
|
|
665
|
+
text: input.text,
|
|
666
|
+
images: images.map((image) => ({
|
|
667
|
+
type: 'image',
|
|
668
|
+
data: image.data,
|
|
669
|
+
mimeType: image.mediaType,
|
|
670
|
+
})),
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
636
674
|
function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
|
|
637
675
|
const texts = new Map<string, string>()
|
|
638
676
|
const inputs = new Map<string, unknown>()
|
|
@@ -937,30 +975,28 @@ async function sessionTools(
|
|
|
937
975
|
) => Promise<AgentToolResult<unknown>>,
|
|
938
976
|
): Promise<ToolDefinition[]> {
|
|
939
977
|
return Promise.all(
|
|
940
|
-
Object.entries(tools)
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
}),
|
|
978
|
+
Object.entries(tools).map(async ([name, tool]): Promise<ToolDefinition> => {
|
|
979
|
+
const builtin = builtinOf(tools, name)
|
|
980
|
+
if (builtin !== undefined)
|
|
981
|
+
// Pi's own definition, for its prompt; A2 already ran the call.
|
|
982
|
+
return Object.assign(builtinDefinition(modules, builtin, cwd), {
|
|
983
|
+
execute: (
|
|
984
|
+
toolCallId: string,
|
|
985
|
+
_params: unknown,
|
|
986
|
+
signal?: AbortSignal,
|
|
987
|
+
) => result(toolCallId, signal, 'pi'),
|
|
988
|
+
})
|
|
989
|
+
return {
|
|
990
|
+
name,
|
|
991
|
+
label: name,
|
|
992
|
+
description:
|
|
993
|
+
typeof tool.description === 'string' ? tool.description : '',
|
|
994
|
+
parameters: (await asSchema(tool.inputSchema)
|
|
995
|
+
.jsonSchema) as ToolDefinition['parameters'],
|
|
996
|
+
execute: (toolCallId, _params, signal) =>
|
|
997
|
+
result(toolCallId, signal, 'tool'),
|
|
998
|
+
}
|
|
999
|
+
}),
|
|
964
1000
|
)
|
|
965
1001
|
}
|
|
966
1002
|
|
|
@@ -1227,6 +1263,26 @@ function thinkingLevel(step: HarnessStep): {
|
|
|
1227
1263
|
return { thinkingLevel: step.reasoning === 'none' ? 'off' : step.reasoning }
|
|
1228
1264
|
}
|
|
1229
1265
|
|
|
1266
|
+
type PayloadHook = NonNullable<
|
|
1267
|
+
NonNullable<Parameters<ModelRuntime['streamSimple']>[2]>['onPayload']
|
|
1268
|
+
>
|
|
1269
|
+
|
|
1270
|
+
/** Asks AI Gateway for the model's fast tier, on the payload Pi's own hook returns. */
|
|
1271
|
+
function fastTier(inner: PayloadHook | undefined): PayloadHook {
|
|
1272
|
+
return async (payload, model) => {
|
|
1273
|
+
const body = ((await inner?.(payload, model)) ?? payload) as {
|
|
1274
|
+
providerOptions?: { gateway?: object }
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
...body,
|
|
1278
|
+
providerOptions: {
|
|
1279
|
+
...body.providerOptions,
|
|
1280
|
+
gateway: { ...body.providerOptions?.gateway, speed: 'fast' },
|
|
1281
|
+
},
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1230
1286
|
function gatewayHeaders(credential: {
|
|
1231
1287
|
token: string
|
|
1232
1288
|
method: string
|
package/src/ai-stored-state.ts
CHANGED
|
@@ -211,9 +211,7 @@ const storeState = <S extends AIStoredView>(state: S): AIStoredState => {
|
|
|
211
211
|
if (
|
|
212
212
|
value === undefined ||
|
|
213
213
|
(typeof value === 'string' && value.length > 80) ||
|
|
214
|
-
(field !== undefined &&
|
|
215
|
-
payloadFields.has(field) &&
|
|
216
|
-
!(field === 'input' && isRecord(value) && 'message' in value)) ||
|
|
214
|
+
(field !== undefined && payloadFields.has(field)) ||
|
|
217
215
|
(isRecord(value) && Object.hasOwn(value, '$a2Value'))
|
|
218
216
|
)
|
|
219
217
|
return intern(value)
|