create-qpq-app 0.1.28 → 0.1.30
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/package.json +2 -2
- package/template/docusaurus/docs/actions/core/ai/ask-ai-get-model-regions.md +50 -0
- package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt-stream.md +3 -3
- package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt.md +34 -19
- package/template/docusaurus/docs/actions/core/log/ask-redact-string.md +46 -0
- package/template/docusaurus/docs/actions/core/secure-token/_category_.json +7 -0
- package/template/docusaurus/docs/actions/core/secure-token/ask-secure-token-generate.md +54 -0
- package/template/docusaurus/docs/actions/features/event-doc-ai/ask-event-doc-ai-stream-turn.md +4 -4
- package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +1 -1
- package/template/docusaurus/docs/config/features/event-doc-ai.md +13 -5
- package/template/docusaurus/docs/config/webserver/web-entry.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-qpq-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Scaffold a new quidproquo app: npx create-qpq-app my-app",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^22.13.13",
|
|
58
|
-
"quidproquo-tsconfig": "0.1.
|
|
58
|
+
"quidproquo-tsconfig": "0.1.30"
|
|
59
59
|
},
|
|
60
60
|
"bin": {
|
|
61
61
|
"create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askAiGetModelRegions
|
|
3
|
+
description: Look up where each AI model processes its requests, so a model picker can show where the data goes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askAiGetModelRegions
|
|
7
|
+
|
|
8
|
+
Resolves with the data regions every [`AiModel`](./ask-ai-prompt.md#aimodel) is offered in on the current platform. Use it to build a model picker that shows the user where a request will be processed, or to refuse a model whose region your data must not reach. The mapping belongs to the platform (on AWS, the Bedrock inference profile behind each model), so a story asks for it instead of hard-coding it.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `AiActionType.GetModelRegions`
|
|
11
|
+
- **On AWS:** reads the regions off the Bedrock model map in the lambda action processor, which holds an inference profile id per model per region. Most entries are `au.` profiles and list only `AiDataRegion.Australia`; Sonnet 5.5, Fable 5 and Fable 5.1 have only a `global.` profile and list only `AiDataRegion.Global`. A prompt takes the Australian profile when the model has one, then Global, then the first region listed, so those three run through their Global profile.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { askAiGetModelRegions, AiDataRegion, AiModel } from 'quidproquo-core';
|
|
15
|
+
|
|
16
|
+
export function* askListAustralianModels(): AskResponse<AiModel[]> {
|
|
17
|
+
const regions = yield* askAiGetModelRegions();
|
|
18
|
+
|
|
19
|
+
return (Object.keys(regions) as AiModel[]).filter((model) => regions[model].includes(AiDataRegion.Australia));
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Signature
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
function* askAiGetModelRegions(): AskResponse<AiModelRegionMap>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Takes no parameters.
|
|
30
|
+
|
|
31
|
+
## Returns
|
|
32
|
+
|
|
33
|
+
`AiModelRegionMap`, a `Record<AiModel, AiDataRegion[]>` with an entry for every enum member. A model may be offered in several regions.
|
|
34
|
+
|
|
35
|
+
### `AiDataRegion`
|
|
36
|
+
|
|
37
|
+
| Member | Meaning |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `Australia` | Requests are processed in Australia (on AWS, an `au.` inference profile). |
|
|
40
|
+
| `Global` | Requests may be processed anywhere the provider chooses, including outside Australia (on AWS, a `global.` inference profile). Sonnet 5.5, Fable 5 and Fable 5.1 are Global only. |
|
|
41
|
+
|
|
42
|
+
## Errors
|
|
43
|
+
|
|
44
|
+
| Error | When |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `ErrorTypeEnum.NotImplemented` | The runtime has no AI action processor (the dev server, for one). |
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
|
|
50
|
+
- [askAiPrompt](./ask-ai-prompt.md) — the `AiModel` table and what each model runs on.
|
|
@@ -42,7 +42,7 @@ The parameters are identical to [askAiPrompt](./ask-ai-prompt.md) — see there
|
|
|
42
42
|
| --- | --- | --- |
|
|
43
43
|
| `model` | `AiModel` | Which model to prompt. |
|
|
44
44
|
| `prompt` | `string` | The user prompt. Ignored when `options.messages` is set. |
|
|
45
|
-
| `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, reasoning?, caching?, maxDurationMs?, maxSteps?, maxOutputTokens? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
|
|
45
|
+
| `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, turnContext?, reasoning?, caching?, cacheTtl?, maxDurationMs?, maxSteps?, maxOutputTokens? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
|
|
46
46
|
|
|
47
47
|
## Returns
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ Within a step, text / reasoning / tool-input events arrive as matched `*Start
|
|
|
63
63
|
| Member (`type`) | When it fires |
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| `Start` (`start`) | Once, before anything else — the response is starting. |
|
|
66
|
-
| `Finish` (`finish`) | Once, at the very end
|
|
66
|
+
| `Finish` (`finish`) | Once, at the very end. Its `usage` is the aggregate across steps and, when caching on Bedrock, carries `cacheReadInputTokens`, `cacheWriteInputTokens` and `noCacheInputTokens`. |
|
|
67
67
|
| `StartStep` (`start-step`) | A generation step (one round-trip to the model) started. |
|
|
68
68
|
| `FinishStep` (`finish-step`) | The current step ended, with its finish reason and usage. |
|
|
69
69
|
| `TextStart` / `TextDelta` / `TextEnd` | Beginning / incremental chunk / end of a text block. |
|
|
@@ -85,7 +85,7 @@ Within a step, text / reasoning / tool-input events arrive as matched `*Start
|
|
|
85
85
|
|
|
86
86
|
### `AiStreamFinishReasonEnum`
|
|
87
87
|
|
|
88
|
-
`Finish.finishReason` and `FinishStep.finishReason` are an `AiStreamFinishReasonEnum` value, not a raw string.
|
|
88
|
+
`Finish.finishReason` and `FinishStep.finishReason` are an `AiStreamFinishReasonEnum` value, not a raw string. Both parts also carry `rawFinishReason`, the provider's own stop reason string when it reported one (on Bedrock, the Converse `stopReason`), which is what to log when the reason is `other` or `unknown`. A raw `refusal`, Anthropic's stop reason when its safety classifiers decline a request, is reported as `contentFilter`.
|
|
89
89
|
|
|
90
90
|
| Member | Wire value | Meaning |
|
|
91
91
|
| --- | --- | --- |
|
|
@@ -48,27 +48,39 @@ function* askAiPrompt(
|
|
|
48
48
|
| `system` | `string` | – | System prompt — high-level instructions that steer the model's behaviour for the whole request. |
|
|
49
49
|
| `aiName` | `string` | – | Name of a [defineAi](../../../config/core/ai.md) config to bind. This is what wires up tool definitions (and their executors) for the model to call. Omit for a plain, tool-less prompt. |
|
|
50
50
|
| `messages` | [`AiMessage[]`](#aimessage) | – | A full conversation history. When present, this is sent instead of `prompt`, letting you carry a multi-turn dialogue (including prior assistant turns and tool results). |
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
51
|
+
| `turnContext` | [`AiMessage[]`](#aimessage) | – | Per-request messages sent after `messages` (or after `prompt`, which then becomes the first user message). They never receive a cache point and are not meant to be saved into a conversation's history, so live state such as a document's current contents belongs here rather than in `system`, where any change rewrites the whole cached conversation. |
|
|
52
|
+
| `reasoning` | [`AiReasoningConfig`](#aireasoningconfig) | – | Enables extended thinking at an effort (`AiReasoningEffort.Low`, `Medium`, `High`, `XHigh`, `Max`). Its presence turns reasoning on. On Claude 4.6 and older the AWS processor turns the effort into a thinking token budget (1,024, 4,096, 8,192, 16,384 or 32,768 tokens). Opus 4.7 and newer and Sonnet 5 run adaptive thinking: the model decides how much to think and the effort steers it. |
|
|
53
|
+
| `caching` | `boolean` | – | Places Bedrock cache points on the system prompt (which also covers the tool definitions) and on the last `messages` entry, and after every tool-calling step on the newest tool message, so the next call in the conversation and the next step in the loop read everything up to there from cache. `turnContext` is never marked. A point only takes effect once the prefix before it reaches the model's minimum: 512 tokens on Claude Opus 5 and 5.5, 1,024 on Sonnet 4.6, Sonnet 5 and Opus 4.8, 4,096 on Opus 4.6, Opus 4.7 and Haiku 4.5. |
|
|
54
|
+
| `cacheTtl` | `AiCacheTtl` | `AiCacheTtl.Dynamic` | Requested lifetime of the request's cache points. `AiCacheTtl.Dynamic` lets the provider choose per point: on Bedrock an hour for the system prompt and the last `messages` entry, so they outlive a pause between turns, and five minutes for the tool-loop point, which is discarded when the call ends. `AiCacheTtl.ProviderDefault` leaves every point on the provider's default (five minutes on Bedrock); `AiCacheTtl.FiveMinutes` keeps every point on five minutes; `AiCacheTtl.OneHour` pins the hour on the system prompt and the last `messages` entry. The tool-loop point is always five minutes, whatever is requested, because it is discarded when the call ends. A request, not a guarantee: a Bedrock model that cannot cache for an hour gets the default instead of a failed request. |
|
|
53
55
|
| `maxSteps` | `number` | – | Cap on model/tool steps in one call. Unset means no cap: the loop runs until the model stops on its own or `maxDurationMs` trips. A client-side tool call (a tool with no executor) still halts it immediately. |
|
|
54
56
|
| `maxOutputTokens` | `number` | provider default | Output token cap per model call. Bedrock defaults to 8192, which a reasoning block plus a large tool input can exceed; the step then finishes with `length` and the tool call arrives truncated. Raise it for agentic workloads (Claude Sonnet allows 64k). |
|
|
55
57
|
| `maxDurationMs` | `number` | – | Wall-clock budget for the tool loop. Checked between steps, so the loop can overrun by one step; leave headroom. When it trips with tool calls still outstanding the result finishes with `toolCalls`, and re-sending the recorded history resumes the turn. Pair it with [askGetRuntimeRemainingTime](../system/ask-get-runtime-remaining-time.md) to stop before the platform deadline. |
|
|
56
58
|
|
|
57
59
|
### `AiModel`
|
|
58
60
|
|
|
59
|
-
The model to run.
|
|
61
|
+
The model to run. On AWS each value maps to a Bedrock cross-region inference profile per data region. A request takes the Australian profile when the model has one, otherwise the Global one, otherwise the first the model lists. A model whose only region is Global is processed wherever Bedrock chooses, which may be outside Australia; check [askAiGetModelRegions](./ask-ai-get-model-regions.md) before offering it where data must stay onshore. [askAiGetModelRegions](./ask-ai-get-model-regions.md) returns the regions each member is offered in, for a model picker.
|
|
60
62
|
|
|
61
|
-
| Member | Model |
|
|
62
|
-
| --- | --- |
|
|
63
|
-
| `ClaudeHaiku35` | Claude 3.5 Haiku |
|
|
64
|
-
| `ClaudeSonnet35` | Claude 3.5 Sonnet |
|
|
65
|
-
| `ClaudeSonnet4` | Claude Sonnet 4 |
|
|
66
|
-
| `ClaudeOpus4` | Claude Opus 4 |
|
|
67
|
-
| `ClaudeHaiku45` | Claude Haiku 4.5 |
|
|
68
|
-
| `ClaudeSonnet45` | Claude Sonnet 4.5 |
|
|
69
|
-
| `ClaudeOpus45` | Claude Opus 4.5 |
|
|
70
|
-
| `ClaudeSonnet46` | Claude Sonnet 4.6 |
|
|
71
|
-
| `ClaudeOpus46` | Claude Opus 4.6 |
|
|
63
|
+
| Member | Model | Data regions |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `ClaudeHaiku35` | Claude 3.5 Haiku | Australia |
|
|
66
|
+
| `ClaudeSonnet35` | Claude 3.5 Sonnet | Australia |
|
|
67
|
+
| `ClaudeSonnet4` | Claude Sonnet 4 | Australia |
|
|
68
|
+
| `ClaudeOpus4` | Claude Opus 4 | Australia |
|
|
69
|
+
| `ClaudeHaiku45` | Claude Haiku 4.5 | Australia |
|
|
70
|
+
| `ClaudeSonnet45` | Claude Sonnet 4.5 | Australia |
|
|
71
|
+
| `ClaudeOpus45` | Claude Opus 4.5 | Australia |
|
|
72
|
+
| `ClaudeSonnet46` | Claude Sonnet 4.6 | Australia |
|
|
73
|
+
| `ClaudeOpus46` | Claude Opus 4.6 | Australia |
|
|
74
|
+
| `ClaudeOpus47` | Claude Opus 4.7 | Australia |
|
|
75
|
+
| `ClaudeOpus48` | Claude Opus 4.8 | Australia |
|
|
76
|
+
| `ClaudeSonnet5` | Claude Sonnet 5 | Australia |
|
|
77
|
+
| `ClaudeOpus5` | Claude Opus 5 | Australia |
|
|
78
|
+
| `ClaudeOpus55` | Claude Opus 5.5 | Australia |
|
|
79
|
+
| `ClaudeSonnet55` | Claude Sonnet 5.5 | Global |
|
|
80
|
+
| `ClaudeFable5` | Claude Fable 5 | Global |
|
|
81
|
+
| `ClaudeFable51` | Claude Fable 5.1 | Global |
|
|
82
|
+
|
|
83
|
+
Bedrock no longer lists an `au.` profile for `ClaudeHaiku35`, `ClaudeSonnet35`, `ClaudeSonnet4`, `ClaudeOpus4` or `ClaudeOpus45` (as of October 2026), so a request on one of them fails at call time with a Bedrock validation error. Sonnet 5.5, Fable 5 and Fable 5.1 have no `au.` profile, so they are Global only.
|
|
72
84
|
|
|
73
85
|
### `AiMessage`
|
|
74
86
|
|
|
@@ -96,20 +108,22 @@ type AiToolMessage = { role: 'tool'; content: AiToolResultPart[] };
|
|
|
96
108
|
### `AiReasoningConfig`
|
|
97
109
|
|
|
98
110
|
```typescript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
111
|
+
type AiReasoningConfig = {
|
|
112
|
+
effort: AiReasoningEffort; // Low | Medium | High | XHigh | Max
|
|
113
|
+
};
|
|
102
114
|
```
|
|
103
115
|
|
|
116
|
+
One knob for every model; what it means per model is under `reasoning` above.
|
|
117
|
+
|
|
104
118
|
## Returns
|
|
105
119
|
|
|
106
|
-
`AiPromptActionResult
|
|
120
|
+
`AiPromptActionResult`: `{ text: string; usage?: AiStreamUsage }`. `text` is the model's complete response; `usage` is the token usage summed across every step when the provider reports it, with `inputTokens`, `outputTokens`, `totalTokens` and, when caching on Bedrock, `cacheReadInputTokens`, `cacheWriteInputTokens` and `noCacheInputTokens`.
|
|
107
121
|
|
|
108
122
|
## Errors
|
|
109
123
|
|
|
110
124
|
| Error | When |
|
|
111
125
|
| --- | --- |
|
|
112
|
-
| `ErrorTypeEnum.NotImplemented` | The `model` has no
|
|
126
|
+
| `ErrorTypeEnum.NotImplemented` | The `model` has no provider model id in any data region. |
|
|
113
127
|
| `ErrorTypeEnum.NotFound` | `options.aiName` names an AI config that does not exist. |
|
|
114
128
|
| `ErrorTypeEnum.GenericError` | Any failure while generating, with the underlying provider message. |
|
|
115
129
|
|
|
@@ -127,5 +141,6 @@ if (!outcome.success) {
|
|
|
127
141
|
## Related
|
|
128
142
|
|
|
129
143
|
- [askAiPromptStream](./ask-ai-prompt-stream.md) — stream the response token-by-token instead of waiting for the whole thing.
|
|
144
|
+
- [askAiGetModelRegions](./ask-ai-get-model-regions.md) — where each model processes its requests, for a model picker.
|
|
130
145
|
- [defineAi](../../../config/core/ai.md) — declares a named AI config with tool definitions the model can call.
|
|
131
146
|
- [defineStorageDrive](../../../config/core/storage-drive.md) — the drive an `AiFileDrivePart` attachment reads from.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askRedactString
|
|
3
|
+
description: Mark a value as sensitive so the admin log redaction removes it from the story's log.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askRedactString
|
|
7
|
+
|
|
8
|
+
Marks a string value as sensitive in this story's log. The admin log redaction removes it from every string in the log, including entries recorded before this call and this call's own entry.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `LogActionType.RedactString`
|
|
11
|
+
- **At runtime:** does nothing. The marking is read later by the admin log redaction when the log is viewed.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { askRedactString } from 'quidproquo-core';
|
|
15
|
+
|
|
16
|
+
export function* askHandleWebhook(signingSecret: string) {
|
|
17
|
+
yield* askRedactString(signingSecret);
|
|
18
|
+
// ...
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Signature
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
function* askRedactString(value: string): AskResponse<void>;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Parameters
|
|
29
|
+
|
|
30
|
+
| Parameter | Type | Default | Description |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `value` | `string` | – | The exact value to remove from the log. |
|
|
33
|
+
|
|
34
|
+
## Returns
|
|
35
|
+
|
|
36
|
+
`void`: the story resumes immediately.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- Only this story's log is covered. A value handed to another service (in a queue message, an email, a service function) must be marked there too.
|
|
41
|
+
- [askSecureTokenGenerate](../secure-token/ask-secure-token-generate.md) results are treated as secrets automatically, so they don't need to be marked.
|
|
42
|
+
|
|
43
|
+
## Related
|
|
44
|
+
|
|
45
|
+
- [askSecureTokenGenerate](../secure-token/ask-secure-token-generate.md): generate a random secret token.
|
|
46
|
+
- [askLogDisableEventHistory](./ask-log-disable-event-history.md): stop the full action history being persisted.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askSecureTokenGenerate
|
|
3
|
+
description: Generate a random token from the platform's secure random source, for use as a secret.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askSecureTokenGenerate
|
|
7
|
+
|
|
8
|
+
Generates a new random token to use as a secret (a signing link, an API secret) and returns it as lowercase hex.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `SecureTokenActionType.Generate`
|
|
11
|
+
- **At runtime:** reads `byteLength` bytes from the platform's Web Crypto random source (`crypto.getRandomValues`) and hex encodes them. There is no `Math.random()` fallback: if no secure source exists, the action fails.
|
|
12
|
+
|
|
13
|
+
The admin log redaction treats the result as a secret. It is blanked in this action's own log entry and swept from every other string in the story's log.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { askSecureTokenGenerate } from 'quidproquo-core';
|
|
17
|
+
|
|
18
|
+
export function* askCreateInviteLink(baseUrl: string) {
|
|
19
|
+
const token = yield* askSecureTokenGenerate();
|
|
20
|
+
return `${baseUrl}/invite/${token}`;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Signature
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
function* askSecureTokenGenerate(byteLength?: number): AskResponse<string>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Default | Description |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `byteLength` | `number` | `32` | Number of random bytes. Must be a whole number from 1 to `SECURE_TOKEN_MAX_BYTE_LENGTH` (1024). |
|
|
35
|
+
|
|
36
|
+
## Returns
|
|
37
|
+
|
|
38
|
+
`string`: the token as lowercase hex, two characters per byte. The default of 32 bytes (256 bits) gives 64 characters.
|
|
39
|
+
|
|
40
|
+
## Errors
|
|
41
|
+
|
|
42
|
+
| Error | Meaning |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `InvalidByteLength` | `byteLength` is not a whole number from 1 to `SECURE_TOKEN_MAX_BYTE_LENGTH`. |
|
|
45
|
+
| `RandomSourceUnavailable` | The runtime has no Web Crypto API. |
|
|
46
|
+
|
|
47
|
+
## Notes
|
|
48
|
+
|
|
49
|
+
- Only this story's log is covered by redaction. If you hand the token to another service (in a queue message, an email, a service function), mark it there too with [askRedactString](../log/ask-redact-string.md).
|
|
50
|
+
|
|
51
|
+
## Related
|
|
52
|
+
|
|
53
|
+
- [askRedactString](../log/ask-redact-string.md): mark any other value as sensitive in the log.
|
|
54
|
+
- [askNewGuid](../guid/ask-new-guid.md): a random id for identifying things, not for use as a secret.
|
package/template/docusaurus/docs/actions/features/event-doc-ai/ask-event-doc-ai-stream-turn.md
CHANGED
|
@@ -14,7 +14,7 @@ import { askEventDocAiStreamTurn } from 'quidproquo-features';
|
|
|
14
14
|
|
|
15
15
|
// A resumed turn: the saved history already ends on the partial assistant reply.
|
|
16
16
|
export function* askResume(docId: string, chatId: string, history: EventDocAiChatMessage[]) {
|
|
17
|
-
return yield* askEventDocAiStreamTurn(docId, chatId, history, true);
|
|
17
|
+
return yield* askEventDocAiStreamTurn(docId, chatId, history, { isContinuation: true, lengthResumes: 0 });
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -36,7 +36,7 @@ function* askEventDocAiStreamTurn(
|
|
|
36
36
|
| `docId` | `string` | The trusted document the chat is scoped to (from session context). |
|
|
37
37
|
| `chatId` | `string` | The chat being replied to. |
|
|
38
38
|
| `history` | `EventDocAiChatMessage[]` | The full saved history to prompt with. Must already be persisted; this story only appends the reply. |
|
|
39
|
-
| `options.isContinuation` | `boolean` | `true` when resuming.
|
|
39
|
+
| `options.isContinuation` | `boolean` | `true` when resuming. Adds the continuation nudge to the turn context, so the request still ends on a user turn (Anthropic rejects a conversation ending on an assistant turn when extended thinking is on). Never saved. |
|
|
40
40
|
| `options.lengthResumes` | `number` | How many consecutive resumes the output token cap has already caused. `0` for a new turn; each `length` handoff passes it on incremented, and the turn stops resuming after three. |
|
|
41
41
|
|
|
42
42
|
## Returns
|
|
@@ -49,9 +49,9 @@ function* askEventDocAiStreamTurn(
|
|
|
49
49
|
## What it does
|
|
50
50
|
|
|
51
51
|
1. Reads the remaining runtime and subtracts the headroom. If nothing is left, hands off immediately.
|
|
52
|
-
2. Resolves the AI name, model, reasoning budget, and system prompt (generator inline function first, else the static prompt, else a default
|
|
52
|
+
2. Resolves the AI name, model, reasoning budget, cache ttl and system prompt (generator inline function first, else the static prompt, else a default). The system prompt heads the cached prefix, so it must come back identical every turn. Then builds the turn context: the `turnContextGenerator` output as a user message, plus the continuation nudge when resuming. Neither is persisted.
|
|
53
53
|
3. Converts the history to model messages. File segments become drive-referenced file parts the action processor resolves at prompt time, under the collection's storage scope. Tools do **not** receive `docId` from the model; executors inherit the session context and read the trusted id there.
|
|
54
|
-
4. Streams with the time budget as `maxDurationMs`, dispatching each part to the UI (`askUIEventDocAiAppendStreamChunk`) as it arrives, except `tool-input-delta` parts. Those are the argument JSON of a tool call streamed in fragments, often hundreds for one call; the UI only needs `tool-input-start` (show "calling X") and `tool-call` (the full input), so the fragments are collected for the saved message but never sent.
|
|
54
|
+
4. Streams the history followed by the turn context (sent as `turnContext`, so it is never cached or saved), with caching on and the time budget as `maxDurationMs`, dispatching each part to the UI (`askUIEventDocAiAppendStreamChunk`) as it arrives, except `tool-input-delta` parts. Those are the argument JSON of a tool call streamed in fragments, often hundreds for one call; the UI only needs `tool-input-start` (show "calling X") and `tool-call` (the full input), so the fragments are collected for the saved message but never sent.
|
|
55
55
|
5. Folds the parts into segments. If any were produced, saves the assistant message and dispatches it as the finalized message (`askUIEventDocAiAppendChatMessage`).
|
|
56
56
|
6. Clears the UI's live-stream buffer and touches the chat (bumps `updatedAt`).
|
|
57
57
|
7. Returns `{ complete: false }` on a pending client tool. Otherwise, if the reply made progress and the finish reason was `toolCalls` (the time budget tripped) or `length` (the output token cap tripped, and fewer than three such resumes have happened), hands off. Otherwise returns `{ complete: true }`, or `{ complete: false }` if the turn was cut off and not resumed.
|
|
@@ -5,7 +5,7 @@ description: Recipient addresses a service may email while its SES account is st
|
|
|
5
5
|
|
|
6
6
|
# defineEmailSenderAllowList
|
|
7
7
|
|
|
8
|
-
Declares recipient addresses the service is allowed to email while the SES account is in **sandbox** mode. In sandbox, SES authorises a send against the recipient's identity as well as the sender's, so the exact-ARN send grant needs each recipient identity listed too. The
|
|
8
|
+
Declares recipient addresses the service is allowed to email while the SES account is in **sandbox** mode. In sandbox, SES authorises a send against the recipient's identity as well as the sender's, so the exact-ARN send grant needs each recipient identity listed too. The deploy also creates each address as an SES email identity. SES then emails the address a confirmation link, and sends to it work once that link is clicked. An address already created outside the stack has to be removed (or imported) first, or the deploy fails because it already exists.
|
|
9
9
|
|
|
10
10
|
This is an AWS-specific concession, not a portable email concept, which is why it lives in `quidproquo-config-aws` beside [defineEmailSender](../webserver/email-sender.md) rather than on that webserver setting. Once the account has SES production access this setting does nothing useful and can be deleted.
|
|
11
11
|
|
|
@@ -58,20 +58,27 @@ All options are a single `EventDocAiOptions` object.
|
|
|
58
58
|
| `aiName` | `string` | `` `${storeName}-ai` `` | Name of the AI registration ([defineAi](../core/ai.md)) the turns prompt through. Override to share or namespace the AI. |
|
|
59
59
|
| `model` | `AiModel` | `AiModel.ClaudeSonnet46` | The model each chat turn is sent to. |
|
|
60
60
|
| `systemPrompt` | `string` | – | A static system prompt for every turn. Used when no `systemPromptGenerator` is set (or the generator returns empty). Falls back to a built-in default (`"You are a helpful assistant. Use tools when appropriate."`) if neither is provided. |
|
|
61
|
-
| `systemPromptGenerator` | `string` | – | Name of a `defineInlineFunction` invoked on **every** turn to build the system prompt. It receives an `EventDocAiSystemPromptInput` (`{ docId }`, the trusted document id) and returns a string
|
|
61
|
+
| `systemPromptGenerator` | `string` | – | Name of a `defineInlineFunction` invoked on **every** turn to build the system prompt. It receives an `EventDocAiSystemPromptInput` (`{ docId }`, the trusted document id) and returns a string. The text must be identical every turn: the system prompt heads the cached prefix, and any change rewrites the whole conversation to cache. Per-turn document state belongs in `turnContextGenerator` or in tools. A non-empty result overrides `systemPrompt`; an empty result falls back to it. |
|
|
62
|
+
| `turnContextGenerator` | `string` | – | Name of a `defineInlineFunction` invoked on **every** turn to build the turn context. Same input as the system prompt generator; the returned text is sent as a user message after the saved history, never persisted and never cached, so this is where live document state goes (a version number and a changed-outside-this-chat note is usually enough when the chat has read tools). An empty result sends no context message. |
|
|
62
63
|
| `tools` | `AiToolDefinition[]` | `[]` | Tools the model may call, registered on the AI. Executors are `defineInlineFunction` names supplied by the caller. Tool runtimes inherit the chat's session context, so they read the trusted `docId` from context rather than trusting the model to pass it. |
|
|
63
|
-
| `
|
|
64
|
+
| `reasoningEffort` | `Nullable<AiReasoningEffort>` | `AiReasoningEffort.Medium` | How hard the model thinks before it answers (`Low`, `Medium`, `High`, `XHigh`, `Max`). Pass `null` to turn thinking off. On Claude 4.6 and older the effort becomes a thinking token budget (1,024 to 32,768 tokens); Opus 4.7 and newer and Sonnet 5 take it directly. Reasoning streams to the chat as `reasoning` segments so the user sees progress instead of a silent wait. |
|
|
64
65
|
| `maxOutputTokens` | `number` | `65536` | Output token cap per model call, defaulting to the Claude Sonnet ceiling on Bedrock (a model with a lower ceiling rejects the request, so lower it for those). The provider default (8192) is too small for a reasoning block plus a large tool input; a call that hits it is cut off mid-JSON. A turn cut off this way is resumed up to three times before it gives up. |
|
|
66
|
+
| `cacheTtl` | `AiCacheTtl` | `AiCacheTtl.Dynamic` | Requested lifetime of the prompt cache entries each turn writes. `AiCacheTtl.Dynamic` keeps the system prompt and the saved history for an hour on Bedrock, so they survive a user's pause between messages, and the tool loop for five minutes, since it is discarded when the turn ends. `AiCacheTtl.FiveMinutes` suits chats with short, rapid turns; `AiCacheTtl.OneHour` pins the hour on the system prompt and saved history. The tool loop is always five minutes, whatever is requested. A model that cannot cache for an hour gets the default instead of a failed request. |
|
|
67
|
+
| `sendUsageToFrontend` | `boolean` | `false` | Whether token usage reaches the browser. Off, the stream's finish parts, the finalised message and the chat history are sent with usage stripped. On, each assistant message arrives with its `usage` and `model`, and `selectEventDocAiChatUsage` sums the open chat. Usage is saved to the history either way. |
|
|
65
68
|
|
|
66
69
|
## The chat model
|
|
67
70
|
|
|
68
71
|
- A **chat** is scoped to one document (`docId`) in the collection. Its summary — `{ docId, chatId, name, createdAt, updatedAt, createdByUserId }` (`EventDocAiChatSummary`) — is a row in the chat-list key-value store (partition `docId`, sort `chatId`).
|
|
69
|
-
- A chat's **message history** is stored as a single JSON file on the chat drive at `` `${docId}/${chatId}/history.json` ``, holding an array of `EventDocAiChatMessage` (`{ role, segments[] }`). Segments are the durable content format: `text`, `reasoning`, `file` (an attachment), and `tool-use` (tool calls paired with results).
|
|
70
|
-
- **Sending a message** appends the user message to the history, streams the assistant's reply through [askAiPromptStream](../../actions/core/ai/ask-ai-prompt-stream.md), dispatches each stream part to the UI live, then folds the completed reply into durable segments, appends it, and bumps the chat's `updatedAt`. Stream parts are transport-only — they never persist; only the folded segments are saved.
|
|
72
|
+
- A chat's **message history** is stored as a single JSON file on the chat drive at `` `${docId}/${chatId}/history.json` ``, holding an array of `EventDocAiChatMessage` (`{ role, segments[] }`). Segments are the durable content format: `text`, `reasoning`, `file` (an attachment), and `tool-use` (tool calls paired with results). Assistant messages also record the `model` that produced them and the `usage` of the execution that did (token counts, cache reads and writes included), so a chat's cost can be worked out from its history and a price table; a turn that spanned several executions is several assistant messages, each with its own usage.
|
|
73
|
+
- **Sending a message** appends the user message to the history, streams the assistant's reply through [askAiPromptStream](../../actions/core/ai/ask-ai-prompt-stream.md), dispatches each stream part to the UI live, then folds the completed reply into durable segments, appends it, and bumps the chat's `updatedAt`. Stream parts are transport-only — they never persist; only the folded segments are saved. The turn context (the `turnContextGenerator` output and, on a resumed turn, the continuation nudge) is sent after the history and is never saved either.
|
|
71
74
|
- **Attachments** are eventDoc assets (uploaded via the collection's asset routes) referenced by bare `assetId`. The send flow validates each id against the session's trusted `docId` before it can reach the model, so a client can never point the AI at another document's files.
|
|
72
75
|
|
|
73
76
|
The four verbs — chat create, list, history, and send — are exposed to the browser as websocket service requests. See the [Event Doc AI actions](../../actions/features/event-doc-ai/ask-event-doc-ai-process-send.md) for the requesters, and the `askUIEventDocAi*` UI actions for driving the chat SPA's state.
|
|
74
77
|
|
|
78
|
+
### Failed, declined and unexplained turns
|
|
79
|
+
|
|
80
|
+
A turn whose AI request fails (an expired credential, a model the account cannot use, a provider outage) is saved with the reply "I am sorry, but I am currently having issues. Please pass this message on to an administrator:" followed by the provider's own error message in quotes. A turn the model declines (the provider reports a content filter, which is also how an Anthropic `refusal` arrives) is saved with the text "The model declined to answer this request." as its reply, so the person sees why nothing came back instead of an empty turn. A turn that stops for a reason the framework cannot name and produced nothing but reasoning is saved with "The model stopped without answering." and the provider's raw stop reason in brackets. Both are ordinary text segments in the history, so the conversation continues normally after them.
|
|
81
|
+
|
|
75
82
|
## Examples
|
|
76
83
|
|
|
77
84
|
```typescript
|
|
@@ -88,10 +95,11 @@ export default [
|
|
|
88
95
|
userDirectoryName: 'users',
|
|
89
96
|
model: AiModel.ClaudeSonnet46,
|
|
90
97
|
systemPromptGenerator: '/entry/ai/buildProjectPrompt::buildProjectPrompt',
|
|
98
|
+
turnContextGenerator: '/entry/ai/buildProjectContext::buildProjectContext',
|
|
91
99
|
tools: [
|
|
92
100
|
/* AiToolDefinition[]; executors registered via defineInlineFunction */
|
|
93
101
|
],
|
|
94
|
-
|
|
102
|
+
reasoningEffort: AiReasoningEffort.High,
|
|
95
103
|
}),
|
|
96
104
|
];
|
|
97
105
|
```
|
|
@@ -73,6 +73,7 @@ export interface WebDomainOptions {
|
|
|
73
73
|
export interface StorageDriveOptions {
|
|
74
74
|
sourceStorageDrive?: string;
|
|
75
75
|
autoUpload: boolean;
|
|
76
|
+
syncServiceViews?: boolean;
|
|
76
77
|
}
|
|
77
78
|
```
|
|
78
79
|
|
|
@@ -80,6 +81,7 @@ export interface StorageDriveOptions {
|
|
|
80
81
|
| --- | --- | --- |
|
|
81
82
|
| `autoUpload` | `boolean` | When `true`, the assets at `buildPath` are uploaded to the origin bucket on every deploy. Set `false` when something else (e.g. a separate publish step) populates the bucket. |
|
|
82
83
|
| `sourceStorageDrive` | `string` (optional) | Serve the app from an **existing** [storage drive](../core/storage-drive.md) (by name) instead of a dedicated bucket created for this web entry. When set, that drive's bucket is used as the CDN origin. |
|
|
84
|
+
| `syncServiceViews` | `boolean` (optional) | Serve the owning service's built views bundle from this entry. `qpq go` copies the service's views build to the root of `sourceStorageDrive`, as well as to its usual prefix in the shared federated views bucket. Makes the service a standalone host on its own domain, the way `shell` is the root site. |
|
|
83
85
|
|
|
84
86
|
### `ResponseSecurityHeaders`
|
|
85
87
|
|