mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(coach): lay down the reasoning, trail and memory contract
Add the shared contract the reasoning release builds on, with no change to how a turn runs today. - Schema: operator reasoning controls on app_settings (0376), the encrypted trail text on coach_messages (0377), and source, last_used_at and source_message_id on coach_facts (0378). - Provider contract: CompletionParams.reasoning and onReasoning, CompletionResult.reasoning and providerState, AiMessage.providerState. - Reasoning levels, thinking budgets and the background-job table in src/lib/ai/reasoning/levels.ts; coachPrefsJson.reasoning, absent meaning medium, salvaged field by field like every other pref. - Stream: activity, memoryNote and planProposal frames, done.stop and done.withheldResults, result.interim, a compare chart kind, eight table refs, three more clarification kinds with an assumed choice, the change_assumption chip, and assumptions, activity, stop, memoryNote and planProposal on the provenance. Zod mirror, type parity and the native-client key rule cover every new frame. - Chat request: memoryDecision and planDecision, parsed and not yet acted on. - Stub contracts for the memory functions and the activity recorder, inert until their implementations land. - Backup, restore and rotation carry the new columns; a fact's message reference is resolved against the restored messages like its conversation reference. - Message keys for the trail, memory, plans, assumptions, replies, interim tables, comparison charts and reasoning settings in all seven locales, each reachable from a catalog and checked both ways.
This commit is contained in:
1 parent
f269100b01
commit
210becb28b
38 files changed
+3308
-57
No files matched your search
+773
-12
@@ -25979,6 +25979,16 @@ components:
|
||||
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
|
||||
switches them off.
|
||||
type: boolean
|
||||
reasoning:
|
||||
description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
|
||||
`medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
|
||||
/api/auth/me` as `coachReasoning`."
|
||||
type: string
|
||||
enum:
|
||||
- off
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
baseUpdatedAt:
|
||||
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
|
||||
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
|
||||
@@ -26666,6 +26676,40 @@ components:
|
||||
maxLength: 8
|
||||
required:
|
||||
- messageId
|
||||
memoryDecision:
|
||||
type: object
|
||||
properties:
|
||||
messageId:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
proposalId:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
accept:
|
||||
type: boolean
|
||||
required:
|
||||
- messageId
|
||||
- proposalId
|
||||
- accept
|
||||
planDecision:
|
||||
type: object
|
||||
properties:
|
||||
messageId:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
planId:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
accept:
|
||||
type: boolean
|
||||
required:
|
||||
- messageId
|
||||
- planId
|
||||
- accept
|
||||
required:
|
||||
- message
|
||||
description: "Inbound Coach turn. `message` is the user's turn (1–4 000 chars). `conversationId` is omitted to start a
|
||||
@@ -34972,6 +35016,14 @@ components:
|
||||
type: boolean
|
||||
assistantDocumentAiEnabled:
|
||||
type: boolean
|
||||
aiReasoningEnabled:
|
||||
type: boolean
|
||||
aiReasoningMaxEffort:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
moduleAvailabilityJson:
|
||||
anyOf:
|
||||
- {}
|
||||
@@ -38866,6 +38918,15 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
trailEncrypted:
|
||||
anyOf:
|
||||
- type: string
|
||||
minLength: 1
|
||||
- type: "null"
|
||||
trailJson:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
createdAt:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -39032,6 +39093,19 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
source:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 32
|
||||
lastUsedAt:
|
||||
anyOf:
|
||||
- type: string
|
||||
minLength: 1
|
||||
- type: "null"
|
||||
sourceMessageId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
createdAt:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -48884,6 +48958,16 @@ components:
|
||||
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
|
||||
switches them off.
|
||||
type: boolean
|
||||
reasoning:
|
||||
description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
|
||||
`medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
|
||||
/api/auth/me` as `coachReasoning`."
|
||||
type: string
|
||||
enum:
|
||||
- off
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
updatedAt:
|
||||
description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as
|
||||
`baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or
|
||||
@@ -52165,6 +52249,12 @@ components:
|
||||
type: string
|
||||
usage:
|
||||
$ref: "#/components/schemas/CoachUsage"
|
||||
stop:
|
||||
$ref: "#/components/schemas/CoachStop"
|
||||
withheldResults:
|
||||
description: v1.41 — the turn was blocked after interim tables went out; remove them.
|
||||
type: boolean
|
||||
const: true
|
||||
required:
|
||||
- type
|
||||
- conversationId
|
||||
@@ -52212,6 +52302,10 @@ components:
|
||||
const: result
|
||||
result:
|
||||
$ref: "#/components/schemas/CoachResultTable"
|
||||
interim:
|
||||
description: v1.41 — sent while the turn still runs.
|
||||
type: boolean
|
||||
const: true
|
||||
required:
|
||||
- type
|
||||
- result
|
||||
@@ -52240,9 +52334,43 @@ components:
|
||||
- type
|
||||
- clarification
|
||||
additionalProperties: false
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
const: activity
|
||||
activity:
|
||||
$ref: "#/components/schemas/CoachActivity"
|
||||
required:
|
||||
- type
|
||||
- activity
|
||||
additionalProperties: false
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
const: memoryNote
|
||||
note:
|
||||
$ref: "#/components/schemas/CoachMemoryNote"
|
||||
required:
|
||||
- type
|
||||
- note
|
||||
additionalProperties: false
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
const: planProposal
|
||||
proposal:
|
||||
$ref: "#/components/schemas/CoachPlanProposal"
|
||||
required:
|
||||
- type
|
||||
- proposal
|
||||
additionalProperties: false
|
||||
description: "One Server-Sent Events frame of a Coach turn, sent as `data: <json>\\n\\n` and dispatched on `type`.
|
||||
Order: `step*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` → `clarification?` →
|
||||
`followUps?` → `done`, or a single `error`. Clients ignore a `type` they do not know."
|
||||
Order: `(activity | step)*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` →
|
||||
`memoryNote?` → `planProposal?` → `clarification?` → `followUps?` → `done`, or a single `error`. `result` frames
|
||||
with `interim: true` may also arrive among the `activity` frames. Clients ignore a `type` they do not know."
|
||||
type: object
|
||||
CoachProvenance:
|
||||
type: object
|
||||
@@ -52451,6 +52579,25 @@ components:
|
||||
that earlier assistant message. A continuation offers no further `continue` chip. Absent otherwise."
|
||||
type: string
|
||||
maxLength: 64
|
||||
activity:
|
||||
description: v1.41 — the turn's trail, metadata only.
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CoachActivityMeta"
|
||||
stop:
|
||||
description: v1.41 — why the answer was forced, when it was.
|
||||
$ref: "#/components/schemas/CoachStop"
|
||||
assumptions:
|
||||
description: v1.41 — what the answer assumed instead of asking, at most two.
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CoachAssumption"
|
||||
memoryNote:
|
||||
description: v1.41 — the fact this reply saved or proposes, without its text.
|
||||
$ref: "#/components/schemas/CoachMemoryNoteMeta"
|
||||
planProposal:
|
||||
description: v1.41 — the plan this reply proposes, without its text.
|
||||
$ref: "#/components/schemas/CoachPlanProposalMeta"
|
||||
required:
|
||||
- windows
|
||||
- metrics
|
||||
@@ -52530,7 +52677,7 @@ components:
|
||||
id:
|
||||
type: string
|
||||
pattern: ^s[1-9]\d{0,2}$
|
||||
description: "`s1`..`s12`, unique within a turn."
|
||||
description: "`s1`..`s48`, unique within a turn."
|
||||
tool:
|
||||
type: string
|
||||
enum:
|
||||
@@ -52598,7 +52745,7 @@ components:
|
||||
resultRef:
|
||||
description: The table this step produced (`r1`..), when it produced one.
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
pattern: ^r[1-8]$
|
||||
required:
|
||||
- id
|
||||
- tool
|
||||
@@ -52724,8 +52871,8 @@ components:
|
||||
properties:
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
description: "`r1`..`r6`, unique within a message."
|
||||
pattern: ^r[1-8]$
|
||||
description: "`r1`..`r8`, unique within a message."
|
||||
source:
|
||||
type: object
|
||||
properties:
|
||||
@@ -52791,6 +52938,7 @@ components:
|
||||
- type: string
|
||||
enum:
|
||||
- line
|
||||
- compare
|
||||
- bar
|
||||
- histogram
|
||||
- type: "null"
|
||||
@@ -52807,7 +52955,7 @@ components:
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
pattern: ^r[1-8]$
|
||||
required:
|
||||
- messageId
|
||||
- ref
|
||||
@@ -52846,6 +52994,7 @@ components:
|
||||
- as_table
|
||||
- related_metric
|
||||
- continue
|
||||
- change_assumption
|
||||
labelKey:
|
||||
type: string
|
||||
label:
|
||||
@@ -52856,7 +53005,7 @@ components:
|
||||
properties:
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^(?:r[1-6])?$
|
||||
pattern: ^(?:r[1-8])?$
|
||||
domain:
|
||||
$ref: "#/components/schemas/CoachStepDomain"
|
||||
window:
|
||||
@@ -52891,6 +53040,91 @@ components:
|
||||
enum:
|
||||
- server
|
||||
- model
|
||||
assumption:
|
||||
description: "v1.41 — on a `change_assumption` chip: the assumption it replaces and the alternative it picks."
|
||||
type: object
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
- metric
|
||||
- window
|
||||
- comparison
|
||||
value:
|
||||
type: object
|
||||
properties:
|
||||
metric:
|
||||
type: string
|
||||
enum:
|
||||
- bp
|
||||
- weight
|
||||
- pulse
|
||||
- mood
|
||||
- compliance
|
||||
- hrv
|
||||
- sleep
|
||||
- resting_hr
|
||||
- steps
|
||||
- active_energy
|
||||
- flights
|
||||
- distance
|
||||
- vo2_max
|
||||
- body_temp
|
||||
- walking_hr
|
||||
- respiratory_rate
|
||||
- spo2
|
||||
- pulse_wave_velocity
|
||||
- vascular_age
|
||||
- body_fat
|
||||
- fat_mass
|
||||
- fat_free_mass
|
||||
- muscle_mass
|
||||
- lean_body_mass
|
||||
- bone_mass
|
||||
- total_body_water
|
||||
- bmi
|
||||
- visceral_fat
|
||||
- glucose
|
||||
- walking_steadiness
|
||||
- walking_asymmetry
|
||||
- walking_double_support
|
||||
- walking_step_length
|
||||
- walking_speed
|
||||
- audio_env
|
||||
- audio_headphone
|
||||
- audio_event
|
||||
- daylight
|
||||
- skin_temp
|
||||
- workouts
|
||||
window:
|
||||
type: string
|
||||
enum:
|
||||
- last7days
|
||||
- last30days
|
||||
- last90days
|
||||
- lastYear
|
||||
- allTime
|
||||
comparison:
|
||||
description: v1.41 — what a comparison is drawn against.
|
||||
type: string
|
||||
enum:
|
||||
- previous_period
|
||||
- year_ago
|
||||
- baseline_90d
|
||||
goal:
|
||||
description: v1.41 — an active plan or a goal fact, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
anchor:
|
||||
description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
additionalProperties: false
|
||||
description: "What a choice stands for: catalog tokens and ids, never text."
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- kind
|
||||
@@ -52909,7 +53143,11 @@ components:
|
||||
enum:
|
||||
- metric
|
||||
- window
|
||||
- comparison
|
||||
- goal
|
||||
- anchor
|
||||
- context
|
||||
description: "`comparison`, `goal` and `anchor` since v1.41."
|
||||
choices:
|
||||
type: array
|
||||
items:
|
||||
@@ -52976,7 +53214,23 @@ components:
|
||||
- last90days
|
||||
- lastYear
|
||||
- allTime
|
||||
comparison:
|
||||
description: v1.41 — what a comparison is drawn against.
|
||||
type: string
|
||||
enum:
|
||||
- previous_period
|
||||
- year_ago
|
||||
- baseline_90d
|
||||
goal:
|
||||
description: v1.41 — an active plan or a goal fact, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
anchor:
|
||||
description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
additionalProperties: false
|
||||
description: "What a choice stands for: catalog tokens and ids, never text."
|
||||
required:
|
||||
- id
|
||||
- labelKey
|
||||
@@ -52987,6 +53241,11 @@ components:
|
||||
freeText:
|
||||
type: boolean
|
||||
description: Whether a typed answer is welcome.
|
||||
assumption:
|
||||
description: v1.41 — the choice that applies when the person does not answer. The question names it, and it is the first
|
||||
choice.
|
||||
type: string
|
||||
pattern: ^c[1-4]$
|
||||
required:
|
||||
- kind
|
||||
- choices
|
||||
@@ -52994,6 +53253,328 @@ components:
|
||||
additionalProperties: false
|
||||
description: "The choices for a clarifying question. The question itself is the assistant message's text. Answer with
|
||||
`clarification: { messageId, choiceId? }`."
|
||||
CoachActivityMeta:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: ^a[1-9]\d?$
|
||||
description: "`a1`..`a99`, unique within a turn."
|
||||
phase:
|
||||
type: string
|
||||
enum:
|
||||
- thinking
|
||||
- memory
|
||||
- fetch
|
||||
- digest
|
||||
- checkpoint
|
||||
- remember
|
||||
- plan
|
||||
- asking
|
||||
- stop
|
||||
- answer
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- running
|
||||
- done
|
||||
- empty
|
||||
- failed
|
||||
round:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: The tool round, from 1.
|
||||
labelKey:
|
||||
type: string
|
||||
description: Closed catalog key (`insights.coach.activity.*`).
|
||||
label:
|
||||
type: string
|
||||
description: Rendered on the server in the request locale from the catalog; never model text.
|
||||
stepRef:
|
||||
description: The `step` a `fetch` entry stands for.
|
||||
type: string
|
||||
pattern: ^s[1-9]\d{0,2}$
|
||||
count:
|
||||
description: Readings, lookups or entries the server counted.
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
durationMs:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
stop:
|
||||
description: Set on the `stop` entry.
|
||||
type: string
|
||||
enum:
|
||||
- budget
|
||||
- time
|
||||
- cap
|
||||
- no_progress
|
||||
required:
|
||||
- id
|
||||
- phase
|
||||
- status
|
||||
- round
|
||||
- labelKey
|
||||
- label
|
||||
additionalProperties: false
|
||||
description: "v1.41 — one entry of a turn's trail as persisted on `metricSource.activity`: phase, status, round, catalog
|
||||
label, counts. The model-written title and text are behind `GET
|
||||
/api/insights/chat/{id}/messages/{messageId}/trail`."
|
||||
CoachStop:
|
||||
type: object
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
enum:
|
||||
- budget
|
||||
- time
|
||||
- cap
|
||||
- no_progress
|
||||
rounds:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: The rounds the turn ran, the final one included.
|
||||
required:
|
||||
- reason
|
||||
- rounds
|
||||
additionalProperties: false
|
||||
description: "v1.41 — why the Coach answered before it was done reading: the token budget, the time budget, the round
|
||||
cap, or no new data."
|
||||
CoachAssumption:
|
||||
type: object
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum:
|
||||
- metric
|
||||
- window
|
||||
- comparison
|
||||
value:
|
||||
type: object
|
||||
properties:
|
||||
labelKey:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
value:
|
||||
type: object
|
||||
properties:
|
||||
metric:
|
||||
type: string
|
||||
enum:
|
||||
- bp
|
||||
- weight
|
||||
- pulse
|
||||
- mood
|
||||
- compliance
|
||||
- hrv
|
||||
- sleep
|
||||
- resting_hr
|
||||
- steps
|
||||
- active_energy
|
||||
- flights
|
||||
- distance
|
||||
- vo2_max
|
||||
- body_temp
|
||||
- walking_hr
|
||||
- respiratory_rate
|
||||
- spo2
|
||||
- pulse_wave_velocity
|
||||
- vascular_age
|
||||
- body_fat
|
||||
- fat_mass
|
||||
- fat_free_mass
|
||||
- muscle_mass
|
||||
- lean_body_mass
|
||||
- bone_mass
|
||||
- total_body_water
|
||||
- bmi
|
||||
- visceral_fat
|
||||
- glucose
|
||||
- walking_steadiness
|
||||
- walking_asymmetry
|
||||
- walking_double_support
|
||||
- walking_step_length
|
||||
- walking_speed
|
||||
- audio_env
|
||||
- audio_headphone
|
||||
- audio_event
|
||||
- daylight
|
||||
- skin_temp
|
||||
- workouts
|
||||
window:
|
||||
type: string
|
||||
enum:
|
||||
- last7days
|
||||
- last30days
|
||||
- last90days
|
||||
- lastYear
|
||||
- allTime
|
||||
comparison:
|
||||
description: v1.41 — what a comparison is drawn against.
|
||||
type: string
|
||||
enum:
|
||||
- previous_period
|
||||
- year_ago
|
||||
- baseline_90d
|
||||
goal:
|
||||
description: v1.41 — an active plan or a goal fact, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
anchor:
|
||||
description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
additionalProperties: false
|
||||
description: "What a choice stands for: catalog tokens and ids, never text."
|
||||
required:
|
||||
- labelKey
|
||||
- label
|
||||
- value
|
||||
additionalProperties: false
|
||||
alternatives:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
labelKey:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
value:
|
||||
type: object
|
||||
properties:
|
||||
metric:
|
||||
type: string
|
||||
enum:
|
||||
- bp
|
||||
- weight
|
||||
- pulse
|
||||
- mood
|
||||
- compliance
|
||||
- hrv
|
||||
- sleep
|
||||
- resting_hr
|
||||
- steps
|
||||
- active_energy
|
||||
- flights
|
||||
- distance
|
||||
- vo2_max
|
||||
- body_temp
|
||||
- walking_hr
|
||||
- respiratory_rate
|
||||
- spo2
|
||||
- pulse_wave_velocity
|
||||
- vascular_age
|
||||
- body_fat
|
||||
- fat_mass
|
||||
- fat_free_mass
|
||||
- muscle_mass
|
||||
- lean_body_mass
|
||||
- bone_mass
|
||||
- total_body_water
|
||||
- bmi
|
||||
- visceral_fat
|
||||
- glucose
|
||||
- walking_steadiness
|
||||
- walking_asymmetry
|
||||
- walking_double_support
|
||||
- walking_step_length
|
||||
- walking_speed
|
||||
- audio_env
|
||||
- audio_headphone
|
||||
- audio_event
|
||||
- daylight
|
||||
- skin_temp
|
||||
- workouts
|
||||
window:
|
||||
type: string
|
||||
enum:
|
||||
- last7days
|
||||
- last30days
|
||||
- last90days
|
||||
- lastYear
|
||||
- allTime
|
||||
comparison:
|
||||
description: v1.41 — what a comparison is drawn against.
|
||||
type: string
|
||||
enum:
|
||||
- previous_period
|
||||
- year_ago
|
||||
- baseline_90d
|
||||
goal:
|
||||
description: v1.41 — an active plan or a goal fact, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
anchor:
|
||||
description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
additionalProperties: false
|
||||
description: "What a choice stands for: catalog tokens and ids, never text."
|
||||
required:
|
||||
- labelKey
|
||||
- label
|
||||
- value
|
||||
additionalProperties: false
|
||||
description: At most 3.
|
||||
required:
|
||||
- kind
|
||||
- value
|
||||
- alternatives
|
||||
additionalProperties: false
|
||||
description: v1.41 — what an answer assumed instead of asking, from a catalog. Each alternative is offered as a
|
||||
`change_assumption` follow-up.
|
||||
CoachMemoryNoteMeta:
|
||||
type: object
|
||||
properties:
|
||||
proposal:
|
||||
type: boolean
|
||||
description: "`true`: waiting for the person, answered with `memoryDecision`. `false`: already saved; undo deletes
|
||||
`factId`."
|
||||
proposalId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
factId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
category:
|
||||
type: string
|
||||
enum:
|
||||
- preference
|
||||
- goal
|
||||
- context
|
||||
- condition
|
||||
- constraint
|
||||
- medication
|
||||
required:
|
||||
- proposal
|
||||
- category
|
||||
additionalProperties: false
|
||||
description: v1.41 — the fact a reply saved or proposes, as persisted on `metricSource.memoryNote`. No fact text.
|
||||
CoachPlanProposalMeta:
|
||||
type: object
|
||||
properties:
|
||||
planId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
metric:
|
||||
type: string
|
||||
maxLength: 64
|
||||
reviewInDays:
|
||||
type: integer
|
||||
minimum: 7
|
||||
maximum: 56
|
||||
required:
|
||||
- planId
|
||||
- metric
|
||||
- reviewInDays
|
||||
additionalProperties: false
|
||||
description: v1.41 — the plan a reply proposes, as persisted on `metricSource.planProposal`. No plan text.
|
||||
CoachUsage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -53028,8 +53609,8 @@ components:
|
||||
properties:
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
description: "`r1`..`r6`, unique within a message."
|
||||
pattern: ^r[1-8]$
|
||||
description: "`r1`..`r8`, unique within a message."
|
||||
source:
|
||||
type: object
|
||||
properties:
|
||||
@@ -53095,6 +53676,7 @@ components:
|
||||
- type: string
|
||||
enum:
|
||||
- line
|
||||
- compare
|
||||
- bar
|
||||
- histogram
|
||||
- type: "null"
|
||||
@@ -53111,7 +53693,7 @@ components:
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
pattern: ^r[1-8]$
|
||||
required:
|
||||
- messageId
|
||||
- ref
|
||||
@@ -53184,6 +53766,38 @@ components:
|
||||
- x
|
||||
- series
|
||||
additionalProperties: false
|
||||
- type: object
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
const: compare
|
||||
mode:
|
||||
type: string
|
||||
enum:
|
||||
- periods
|
||||
- metrics
|
||||
x:
|
||||
type: string
|
||||
a:
|
||||
type: string
|
||||
b:
|
||||
type: string
|
||||
axes:
|
||||
anyOf:
|
||||
- type: number
|
||||
const: 1
|
||||
- type: number
|
||||
const: 2
|
||||
required:
|
||||
- kind
|
||||
- mode
|
||||
- x
|
||||
- a
|
||||
- b
|
||||
- axes
|
||||
additionalProperties: false
|
||||
description: "v1.41 — two series on one chart: two periods overlaid (`periods`, one axis) or two metrics (`metrics`, one
|
||||
or two axes). `a` and `b` name the value columns, `x` the shared column."
|
||||
- type: object
|
||||
properties:
|
||||
kind:
|
||||
@@ -53257,6 +53871,143 @@ components:
|
||||
additionalProperties: false
|
||||
description: "A table of the values a turn read. Health data: encrypted at rest and sent only to the account that owns
|
||||
the conversation."
|
||||
CoachActivity:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: ^a[1-9]\d?$
|
||||
description: "`a1`..`a99`, unique within a turn."
|
||||
phase:
|
||||
type: string
|
||||
enum:
|
||||
- thinking
|
||||
- memory
|
||||
- fetch
|
||||
- digest
|
||||
- checkpoint
|
||||
- remember
|
||||
- plan
|
||||
- asking
|
||||
- stop
|
||||
- answer
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- running
|
||||
- done
|
||||
- empty
|
||||
- failed
|
||||
round:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: The tool round, from 1.
|
||||
labelKey:
|
||||
type: string
|
||||
description: Closed catalog key (`insights.coach.activity.*`).
|
||||
label:
|
||||
type: string
|
||||
description: Rendered on the server in the request locale from the catalog; never model text.
|
||||
stepRef:
|
||||
description: The `step` a `fetch` entry stands for.
|
||||
type: string
|
||||
pattern: ^s[1-9]\d{0,2}$
|
||||
count:
|
||||
description: Readings, lookups or entries the server counted.
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
durationMs:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
stop:
|
||||
description: Set on the `stop` entry.
|
||||
type: string
|
||||
enum:
|
||||
- budget
|
||||
- time
|
||||
- cap
|
||||
- no_progress
|
||||
title:
|
||||
description: A screened reasoning title or checkpoint sentence.
|
||||
type: string
|
||||
maxLength: 80
|
||||
text:
|
||||
description: A screened reasoning summary for the round.
|
||||
type: string
|
||||
maxLength: 400
|
||||
required:
|
||||
- id
|
||||
- phase
|
||||
- status
|
||||
- round
|
||||
- labelKey
|
||||
- label
|
||||
additionalProperties: false
|
||||
description: v1.41 — one entry of the live trail, sent as `activity` frames that upsert by `id`. `title` and `text` are
|
||||
model text, screened, and sent only to the account that owns the conversation.
|
||||
CoachMemoryNote:
|
||||
type: object
|
||||
properties:
|
||||
proposal:
|
||||
type: boolean
|
||||
description: "`true`: waiting for the person, answered with `memoryDecision`. `false`: already saved; undo deletes
|
||||
`factId`."
|
||||
proposalId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
factId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
category:
|
||||
type: string
|
||||
enum:
|
||||
- preference
|
||||
- goal
|
||||
- context
|
||||
- condition
|
||||
- constraint
|
||||
- medication
|
||||
fact:
|
||||
type: string
|
||||
maxLength: 160
|
||||
required:
|
||||
- proposal
|
||||
- category
|
||||
- fact
|
||||
additionalProperties: false
|
||||
description: v1.41 — a fact the Coach saved during the turn, or proposes to save (health facts are never saved without a
|
||||
tap). Owner only.
|
||||
CoachPlanProposal:
|
||||
type: object
|
||||
properties:
|
||||
planId:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_-]{1,64}$
|
||||
metric:
|
||||
type: string
|
||||
maxLength: 64
|
||||
reviewInDays:
|
||||
type: integer
|
||||
minimum: 7
|
||||
maximum: 56
|
||||
ifCue:
|
||||
type: string
|
||||
thenAction:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
required:
|
||||
- planId
|
||||
- metric
|
||||
- reviewInDays
|
||||
- ifCue
|
||||
- thenAction
|
||||
additionalProperties: false
|
||||
description: "v1.41 — a plan the Coach proposes, written as `proposed`. Answer with `planDecision: { messageId, planId,
|
||||
accept }`. Owner only."
|
||||
CoachMessageResultsResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -53289,7 +54040,7 @@ components:
|
||||
properties:
|
||||
ref:
|
||||
type: string
|
||||
pattern: ^r[1-6]$
|
||||
pattern: ^r[1-8]$
|
||||
withheld:
|
||||
type: string
|
||||
enum:
|
||||
@@ -78175,6 +78926,16 @@ components:
|
||||
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
|
||||
switches them off.
|
||||
type: boolean
|
||||
reasoning:
|
||||
description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
|
||||
`medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
|
||||
/api/auth/me` as `coachReasoning`."
|
||||
type: string
|
||||
enum:
|
||||
- off
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
required:
|
||||
- tone
|
||||
- verbosity
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "{count} Bereiche angesehen",
|
||||
"info": "Details zur Antwort",
|
||||
"infoModel": "Modell: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Denkt nach…",
|
||||
"fetching": "Holt {domain}, {window}…",
|
||||
"digestOne": "Fasst {count} Wert zusammen…",
|
||||
"digestFew": "Fasst {count} Werte zusammen…",
|
||||
"digestOther": "Fasst {count} Werte zusammen…",
|
||||
"digestDoneOne": "{count} Wert aus {areas}",
|
||||
"digestDoneFew": "{count} Werte aus {areas}",
|
||||
"digestDoneOther": "{count} Werte aus {areas}",
|
||||
"areasOne": "{count} Bereich",
|
||||
"areasFew": "{count} Bereichen",
|
||||
"areasOther": "{count} Bereichen",
|
||||
"memoryOne": "Erinnert sich an {count} Eintrag",
|
||||
"memoryFew": "Erinnert sich an {count} Einträge",
|
||||
"memoryOther": "Erinnert sich an {count} Einträge",
|
||||
"remember": "Merkt sich etwas…",
|
||||
"plan": "Schlägt einen Plan vor…",
|
||||
"asking": "Fragt nach…",
|
||||
"answer": "Schreibt die Antwort…",
|
||||
"summaryOne": "Nachgedacht, {lookups} Abfrage, {seconds} s",
|
||||
"summaryFew": "Nachgedacht, {lookups} Abfragen, {seconds} s",
|
||||
"summaryOther": "Nachgedacht, {lookups} Abfragen, {seconds} s",
|
||||
"summaryNoLookups": "Nachgedacht, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Budget erreicht, antwortet mit dem, was da ist",
|
||||
"time": "Zeit erreicht, antwortet mit dem, was da ist",
|
||||
"cap": "Viele Schritte, fasst jetzt zusammen",
|
||||
"noProgress": "Keine neuen Daten mehr, fasst zusammen"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Gemerkt: {fact}",
|
||||
"undo": "Rückgängig",
|
||||
"accept": "Ja, merken",
|
||||
"decline": "Nein",
|
||||
"confirmed": "Gemerkt."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Plan übernehmen",
|
||||
"decline": "Nicht jetzt",
|
||||
"confirmed": "Ist notiert. Ich schaue in {days} Tagen mit dir drauf."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Angenommen: {value}",
|
||||
"change": "Ändern"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Antwortvorschläge"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} Wert",
|
||||
"labelFew": "{title}, {window}, {count} Werte",
|
||||
"labelOther": "{title}, {window}, {count} Werte"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} gegen {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Denktiefe",
|
||||
"hint": "Gründlicher denken heißt genauere Antworten und etwas mehr Wartezeit.",
|
||||
"off": "Aus",
|
||||
"minimal": "Minimal",
|
||||
"low": "Niedrig",
|
||||
"medium": "Mittel",
|
||||
"high": "Hoch",
|
||||
"capped": "(vom Admin begrenzt)",
|
||||
"disabled": "Der Admin hat gründliches Denken abgeschaltet."
|
||||
},
|
||||
"memoryLink": "Gedächtnis ({count})"
|
||||
},
|
||||
"relativeJustNow": "gerade eben",
|
||||
"relativeMinutesAgoOne": "vor {count} Minute",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "Konnte diese Angabe nicht vergessen. Bitte erneut versuchen.",
|
||||
"clearedToast": "{count} gespeicherte Angaben vergessen.",
|
||||
"clearError": "Konnte die gespeicherten Angaben nicht löschen. Bitte erneut versuchen.",
|
||||
"detail": "Er nutzt sie, damit seine Antworten relevant bleiben. Entferne alles, was er besser vergessen sollte."
|
||||
"detail": "Er nutzt sie, damit seine Antworten relevant bleiben. Entferne alles, was er besser vergessen sollte.",
|
||||
"categoryMedication": "Medikamente"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "Deine Erinnerungen",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach ist aktiviert.",
|
||||
"savedOff": "Coach ist ausgeblendet.",
|
||||
"saveError": "Speichern fehlgeschlagen. Bitte erneut versuchen."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Bearbeiten",
|
||||
"source": {
|
||||
"user": "von dir",
|
||||
"coach": "vom Coach"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Dokumente lesen",
|
||||
"description": "KI-Lesen gespeicherter Dokumente, Scans von Laborbefunden und das Erkennen von Medikamenten aus Text. Der größte Anteil der Daten, die den Server verlassen."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Gründliches Denken erlauben",
|
||||
"max": "Höchste Denktiefe"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "Siehe auch: {metric}",
|
||||
"continue": "Weitersuchen",
|
||||
"settingLabel": "Anschlussfragen vorschlagen",
|
||||
"settingHint": "Zeigt unter der letzten Antwort des Coaches bis zu drei Vorschläge für die nächste Frage."
|
||||
"settingHint": "Zeigt unter der letzten Antwort des Coaches bis zu drei Vorschläge für die nächste Frage.",
|
||||
"changeAssumption": "Stattdessen: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "Der Coach hat eine Rückfrage",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "Looked at {count} areas",
|
||||
"info": "Answer details",
|
||||
"infoModel": "Model: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Thinking…",
|
||||
"fetching": "Fetching {domain}, {window}…",
|
||||
"digestOne": "Summarising {count} reading…",
|
||||
"digestFew": "Summarising {count} readings…",
|
||||
"digestOther": "Summarising {count} readings…",
|
||||
"digestDoneOne": "{count} reading from {areas}",
|
||||
"digestDoneFew": "{count} readings from {areas}",
|
||||
"digestDoneOther": "{count} readings from {areas}",
|
||||
"areasOne": "{count} area",
|
||||
"areasFew": "{count} areas",
|
||||
"areasOther": "{count} areas",
|
||||
"memoryOne": "Recalls {count} thing you told it",
|
||||
"memoryFew": "Recalls {count} things you told it",
|
||||
"memoryOther": "Recalls {count} things you told it",
|
||||
"remember": "Remembering something…",
|
||||
"plan": "Drafting a plan…",
|
||||
"asking": "Asking you…",
|
||||
"answer": "Writing the answer…",
|
||||
"summaryOne": "Thought it through, {lookups} lookup, {seconds} s",
|
||||
"summaryFew": "Thought it through, {lookups} lookups, {seconds} s",
|
||||
"summaryOther": "Thought it through, {lookups} lookups, {seconds} s",
|
||||
"summaryNoLookups": "Thought it through, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Budget reached, answering with what it has",
|
||||
"time": "Time limit reached, answering with what it has",
|
||||
"cap": "Many steps, wrapping up",
|
||||
"noProgress": "No new data, wrapping up"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Remembered: {fact}",
|
||||
"undo": "Undo",
|
||||
"accept": "Yes, remember it",
|
||||
"decline": "No",
|
||||
"confirmed": "Got it, I'll remember."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Take on this plan",
|
||||
"decline": "Not now",
|
||||
"confirmed": "Noted. I'll check in with you in {days} days."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Assumed: {value}",
|
||||
"change": "Change"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Suggested replies"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} reading",
|
||||
"labelFew": "{title}, {window}, {count} readings",
|
||||
"labelOther": "{title}, {window}, {count} readings"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} vs {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Thinking depth",
|
||||
"hint": "Deeper thinking means more precise answers and a little more waiting.",
|
||||
"off": "Off",
|
||||
"minimal": "Minimal",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High",
|
||||
"capped": "(limited by the admin)",
|
||||
"disabled": "The admin has turned deeper thinking off."
|
||||
},
|
||||
"memoryLink": "Memory ({count})"
|
||||
},
|
||||
"relativeJustNow": "just now",
|
||||
"relativeMinutesAgoOne": "{count} minute ago",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "Could not forget that fact. Please try again.",
|
||||
"clearedToast": "Forgot {count} stored facts.",
|
||||
"clearError": "Could not clear the stored facts. Please try again.",
|
||||
"detail": "It uses these to keep its replies relevant. Remove anything you would rather it forgot."
|
||||
"detail": "It uses these to keep its replies relevant. Remove anything you would rather it forgot.",
|
||||
"categoryMedication": "Medications"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "Your reminders",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach activated.",
|
||||
"savedOff": "Coach hidden.",
|
||||
"saveError": "Could not save the change. Please try again."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Edit",
|
||||
"source": {
|
||||
"user": "from you",
|
||||
"coach": "from the Coach"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Reading documents",
|
||||
"description": "AI reading of stored documents, lab report scans and medication extraction from text. The largest share of data leaving the server."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Allow deeper thinking",
|
||||
"max": "Highest thinking depth"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "See also: {metric}",
|
||||
"continue": "Keep looking",
|
||||
"settingLabel": "Suggest follow-up questions",
|
||||
"settingHint": "Show up to three follow-up suggestions under the Coach's latest answer."
|
||||
"settingHint": "Show up to three follow-up suggestions under the Coach's latest answer.",
|
||||
"changeAssumption": "Instead: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "The Coach has a question",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "{count} áreas consultadas",
|
||||
"info": "Detalles de la respuesta",
|
||||
"infoModel": "Modelo: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Pensando…",
|
||||
"fetching": "Consultando {domain}, {window}…",
|
||||
"digestOne": "Resumiendo {count} valor…",
|
||||
"digestFew": "Resumiendo {count} valores…",
|
||||
"digestOther": "Resumiendo {count} valores…",
|
||||
"digestDoneOne": "{count} valor de {areas}",
|
||||
"digestDoneFew": "{count} valores de {areas}",
|
||||
"digestDoneOther": "{count} valores de {areas}",
|
||||
"areasOne": "{count} área",
|
||||
"areasFew": "{count} áreas",
|
||||
"areasOther": "{count} áreas",
|
||||
"memoryOne": "Recuerda {count} cosa que le contaste",
|
||||
"memoryFew": "Recuerda {count} cosas que le contaste",
|
||||
"memoryOther": "Recuerda {count} cosas que le contaste",
|
||||
"remember": "Recordando algo…",
|
||||
"plan": "Preparando un plan…",
|
||||
"asking": "Te pregunta…",
|
||||
"answer": "Escribiendo la respuesta…",
|
||||
"summaryOne": "Lo ha pensado, {lookups} consulta, {seconds} s",
|
||||
"summaryFew": "Lo ha pensado, {lookups} consultas, {seconds} s",
|
||||
"summaryOther": "Lo ha pensado, {lookups} consultas, {seconds} s",
|
||||
"summaryNoLookups": "Lo ha pensado, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Presupuesto alcanzado, responde con lo que tiene",
|
||||
"time": "Tiempo agotado, responde con lo que tiene",
|
||||
"cap": "Muchos pasos, ahora lo resume",
|
||||
"noProgress": "No hay datos nuevos, lo resume"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Recordado: {fact}",
|
||||
"undo": "Deshacer",
|
||||
"accept": "Sí, recuérdalo",
|
||||
"decline": "No",
|
||||
"confirmed": "Entendido, lo recordaré."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Adoptar este plan",
|
||||
"decline": "Ahora no",
|
||||
"confirmed": "Anotado. Lo revisaremos juntos dentro de {days} días."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Supuesto: {value}",
|
||||
"change": "Cambiar"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Respuestas sugeridas"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} valor",
|
||||
"labelFew": "{title}, {window}, {count} valores",
|
||||
"labelOther": "{title}, {window}, {count} valores"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} frente a {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Profundidad de razonamiento",
|
||||
"hint": "Pensar más a fondo significa respuestas más precisas y un poco más de espera.",
|
||||
"off": "Desactivado",
|
||||
"minimal": "Mínimo",
|
||||
"low": "Bajo",
|
||||
"medium": "Medio",
|
||||
"high": "Alto",
|
||||
"capped": "(limitado por el administrador)",
|
||||
"disabled": "El administrador ha desactivado el razonamiento profundo."
|
||||
},
|
||||
"memoryLink": "Memoria ({count})"
|
||||
},
|
||||
"relativeJustNow": "ahora mismo",
|
||||
"relativeMinutesAgoOne": "hace {count} minuto",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "No se pudo olvidar ese dato. Inténtalo de nuevo.",
|
||||
"clearedToast": "Se olvidaron {count} datos guardados.",
|
||||
"clearError": "No se pudieron borrar los datos guardados. Inténtalo de nuevo.",
|
||||
"detail": "Los usa para que sus respuestas sigan siendo relevantes. Elimina lo que prefieras que olvide."
|
||||
"detail": "Los usa para que sus respuestas sigan siendo relevantes. Elimina lo que prefieras que olvide.",
|
||||
"categoryMedication": "Medicamentos"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "Tus recordatorios",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach activado.",
|
||||
"savedOff": "Coach oculto.",
|
||||
"saveError": "No se pudo guardar el cambio. Inténtalo de nuevo."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Editar",
|
||||
"source": {
|
||||
"user": "de ti",
|
||||
"coach": "del Coach"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Lectura de documentos",
|
||||
"description": "Lectura con IA de documentos guardados, escaneo de informes de laboratorio y extracción de medicamentos a partir de texto. La mayor parte de los datos que salen del servidor."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Permitir razonamiento profundo",
|
||||
"max": "Profundidad máxima de razonamiento"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "Ver también: {metric}",
|
||||
"continue": "Seguir buscando",
|
||||
"settingLabel": "Sugerir preguntas de seguimiento",
|
||||
"settingHint": "Muestra hasta tres sugerencias bajo la última respuesta del Coach."
|
||||
"settingHint": "Muestra hasta tres sugerencias bajo la última respuesta del Coach.",
|
||||
"changeAssumption": "En su lugar: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "El Coach tiene una pregunta",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "{count} domaines consultés",
|
||||
"info": "Détails de la réponse",
|
||||
"infoModel": "Modèle : {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Réfléchit…",
|
||||
"fetching": "Récupère {domain}, {window}…",
|
||||
"digestOne": "Résume {count} mesure…",
|
||||
"digestFew": "Résume {count} mesures…",
|
||||
"digestOther": "Résume {count} mesures…",
|
||||
"digestDoneOne": "{count} mesure sur {areas}",
|
||||
"digestDoneFew": "{count} mesures sur {areas}",
|
||||
"digestDoneOther": "{count} mesures sur {areas}",
|
||||
"areasOne": "{count} domaine",
|
||||
"areasFew": "{count} domaines",
|
||||
"areasOther": "{count} domaines",
|
||||
"memoryOne": "Se souvient de {count} chose que tu lui as dite",
|
||||
"memoryFew": "Se souvient de {count} choses que tu lui as dites",
|
||||
"memoryOther": "Se souvient de {count} choses que tu lui as dites",
|
||||
"remember": "Retient quelque chose…",
|
||||
"plan": "Propose un plan…",
|
||||
"asking": "Te pose une question…",
|
||||
"answer": "Rédige la réponse…",
|
||||
"summaryOne": "Réflexion faite, {lookups} recherche, {seconds} s",
|
||||
"summaryFew": "Réflexion faite, {lookups} recherches, {seconds} s",
|
||||
"summaryOther": "Réflexion faite, {lookups} recherches, {seconds} s",
|
||||
"summaryNoLookups": "Réflexion faite, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Budget atteint, répond avec ce qu'il a",
|
||||
"time": "Temps écoulé, répond avec ce qu'il a",
|
||||
"cap": "Beaucoup d'étapes, conclut maintenant",
|
||||
"noProgress": "Plus de nouvelles données, conclut"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Retenu : {fact}",
|
||||
"undo": "Annuler",
|
||||
"accept": "Oui, retiens-le",
|
||||
"decline": "Non",
|
||||
"confirmed": "C'est noté, je m'en souviendrai."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Adopter ce plan",
|
||||
"decline": "Pas maintenant",
|
||||
"confirmed": "C'est noté. On fait le point ensemble dans {days} jours."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Hypothèse : {value}",
|
||||
"change": "Modifier"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Réponses suggérées"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} mesure",
|
||||
"labelFew": "{title}, {window}, {count} mesures",
|
||||
"labelOther": "{title}, {window}, {count} mesures"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} contre {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Profondeur de réflexion",
|
||||
"hint": "Réfléchir plus en profondeur donne des réponses plus précises, avec un peu plus d'attente.",
|
||||
"off": "Désactivé",
|
||||
"minimal": "Minimale",
|
||||
"low": "Faible",
|
||||
"medium": "Moyenne",
|
||||
"high": "Élevée",
|
||||
"capped": "(limité par l'administrateur)",
|
||||
"disabled": "L'administrateur a désactivé la réflexion approfondie."
|
||||
},
|
||||
"memoryLink": "Mémoire ({count})"
|
||||
},
|
||||
"relativeJustNow": "à l’instant",
|
||||
"relativeMinutesAgoOne": "il y a {count} minute",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "Impossible d'oublier cette information. Veuillez réessayer.",
|
||||
"clearedToast": "{count} informations enregistrées oubliées.",
|
||||
"clearError": "Impossible d'effacer les informations enregistrées. Veuillez réessayer.",
|
||||
"detail": "Il s'en sert pour garder ses réponses pertinentes. Supprimez tout ce qu'il vaut mieux qu'il oublie."
|
||||
"detail": "Il s'en sert pour garder ses réponses pertinentes. Supprimez tout ce qu'il vaut mieux qu'il oublie.",
|
||||
"categoryMedication": "Médicaments"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "Tes rappels",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach activé.",
|
||||
"savedOff": "Coach masqué.",
|
||||
"saveError": "Impossible d enregistrer la modification. Veuillez réessayer."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Modifier",
|
||||
"source": {
|
||||
"user": "de toi",
|
||||
"coach": "du Coach"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Lecture de documents",
|
||||
"description": "Lecture par IA des documents enregistrés, scan des comptes rendus de laboratoire et extraction de médicaments à partir de texte. La plus grande part des données qui quittent le serveur."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Autoriser la réflexion approfondie",
|
||||
"max": "Profondeur de réflexion maximale"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "Voir aussi : {metric}",
|
||||
"continue": "Continuer à chercher",
|
||||
"settingLabel": "Suggérer des questions de suivi",
|
||||
"settingHint": "Affiche jusqu'à trois suggestions sous la dernière réponse du Coach."
|
||||
"settingHint": "Affiche jusqu'à trois suggestions sous la dernière réponse du Coach.",
|
||||
"changeAssumption": "À la place : {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "Le Coach a une question",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "{count} aree consultate",
|
||||
"info": "Dettagli della risposta",
|
||||
"infoModel": "Modello: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Sta pensando…",
|
||||
"fetching": "Recupera {domain}, {window}…",
|
||||
"digestOne": "Riassume {count} valore…",
|
||||
"digestFew": "Riassume {count} valori…",
|
||||
"digestOther": "Riassume {count} valori…",
|
||||
"digestDoneOne": "{count} valore da {areas}",
|
||||
"digestDoneFew": "{count} valori da {areas}",
|
||||
"digestDoneOther": "{count} valori da {areas}",
|
||||
"areasOne": "{count} area",
|
||||
"areasFew": "{count} aree",
|
||||
"areasOther": "{count} aree",
|
||||
"memoryOne": "Ricorda {count} cosa che gli hai detto",
|
||||
"memoryFew": "Ricorda {count} cose che gli hai detto",
|
||||
"memoryOther": "Ricorda {count} cose che gli hai detto",
|
||||
"remember": "Memorizza qualcosa…",
|
||||
"plan": "Prepara un piano…",
|
||||
"asking": "Ti fa una domanda…",
|
||||
"answer": "Scrive la risposta…",
|
||||
"summaryOne": "Ci ha pensato, {lookups} ricerca, {seconds} s",
|
||||
"summaryFew": "Ci ha pensato, {lookups} ricerche, {seconds} s",
|
||||
"summaryOther": "Ci ha pensato, {lookups} ricerche, {seconds} s",
|
||||
"summaryNoLookups": "Ci ha pensato, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Budget raggiunto, risponde con ciò che ha",
|
||||
"time": "Tempo scaduto, risponde con ciò che ha",
|
||||
"cap": "Molti passaggi, ora riassume",
|
||||
"noProgress": "Nessun dato nuovo, riassume"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Memorizzato: {fact}",
|
||||
"undo": "Annulla",
|
||||
"accept": "Sì, ricordalo",
|
||||
"decline": "No",
|
||||
"confirmed": "Ricevuto, me lo ricorderò."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Adotta questo piano",
|
||||
"decline": "Non ora",
|
||||
"confirmed": "Annotato. Ne riparliamo insieme tra {days} giorni."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Ipotesi: {value}",
|
||||
"change": "Cambia"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Risposte suggerite"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} valore",
|
||||
"labelFew": "{title}, {window}, {count} valori",
|
||||
"labelOther": "{title}, {window}, {count} valori"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} rispetto a {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Profondità di ragionamento",
|
||||
"hint": "Pensare più a fondo significa risposte più precise e un po' più di attesa.",
|
||||
"off": "Disattivato",
|
||||
"minimal": "Minima",
|
||||
"low": "Bassa",
|
||||
"medium": "Media",
|
||||
"high": "Alta",
|
||||
"capped": "(limitato dall'amministratore)",
|
||||
"disabled": "L'amministratore ha disattivato il ragionamento approfondito."
|
||||
},
|
||||
"memoryLink": "Memoria ({count})"
|
||||
},
|
||||
"relativeJustNow": "proprio ora",
|
||||
"relativeMinutesAgoOne": "{count} minuto fa",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "Impossibile dimenticare questa informazione. Riprova.",
|
||||
"clearedToast": "Dimenticate {count} informazioni memorizzate.",
|
||||
"clearError": "Impossibile cancellare le informazioni memorizzate. Riprova.",
|
||||
"detail": "Le usa per mantenere pertinenti le sue risposte. Rimuovi tutto ciò che preferisci dimentichi."
|
||||
"detail": "Le usa per mantenere pertinenti le sue risposte. Rimuovi tutto ciò che preferisci dimentichi.",
|
||||
"categoryMedication": "Farmaci"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "I tuoi promemoria",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach attivato.",
|
||||
"savedOff": "Coach nascosto.",
|
||||
"saveError": "Impossibile salvare la modifica. Riprova."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Modifica",
|
||||
"source": {
|
||||
"user": "da te",
|
||||
"coach": "dal Coach"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Lettura dei documenti",
|
||||
"description": "Lettura con IA dei documenti salvati, scansione dei referti di laboratorio ed estrazione dei farmaci dal testo. La quota maggiore dei dati che lasciano il server."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Consenti ragionamento approfondito",
|
||||
"max": "Profondità massima di ragionamento"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "Vedi anche: {metric}",
|
||||
"continue": "Continua a cercare",
|
||||
"settingLabel": "Suggerisci domande di approfondimento",
|
||||
"settingHint": "Mostra fino a tre suggerimenti sotto l'ultima risposta del Coach."
|
||||
"settingHint": "Mostra fino a tre suggerimenti sotto l'ultima risposta del Coach.",
|
||||
"changeAssumption": "Invece: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "Il Coach ha una domanda",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "영역 {count}개 확인",
|
||||
"info": "답변 정보",
|
||||
"infoModel": "모델: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "생각하는 중…",
|
||||
"fetching": "{domain} 가져오는 중, {window}…",
|
||||
"digestOne": "값 {count}개 요약 중…",
|
||||
"digestFew": "값 {count}개 요약 중…",
|
||||
"digestOther": "값 {count}개 요약 중…",
|
||||
"digestDoneOne": "{areas}에서 값 {count}개",
|
||||
"digestDoneFew": "{areas}에서 값 {count}개",
|
||||
"digestDoneOther": "{areas}에서 값 {count}개",
|
||||
"areasOne": "{count}개 영역",
|
||||
"areasFew": "{count}개 영역",
|
||||
"areasOther": "{count}개 영역",
|
||||
"memoryOne": "알려 준 내용 {count}개를 떠올리는 중",
|
||||
"memoryFew": "알려 준 내용 {count}개를 떠올리는 중",
|
||||
"memoryOther": "알려 준 내용 {count}개를 떠올리는 중",
|
||||
"remember": "기억해 두는 중…",
|
||||
"plan": "계획을 제안하는 중…",
|
||||
"asking": "질문하는 중…",
|
||||
"answer": "답변을 쓰는 중…",
|
||||
"summaryOne": "생각 완료, 조회 {lookups}회, {seconds}초",
|
||||
"summaryFew": "생각 완료, 조회 {lookups}회, {seconds}초",
|
||||
"summaryOther": "생각 완료, 조회 {lookups}회, {seconds}초",
|
||||
"summaryNoLookups": "생각 완료, {seconds}초",
|
||||
"stop": {
|
||||
"budget": "예산 도달, 지금까지의 내용으로 답변",
|
||||
"time": "시간 제한 도달, 지금까지의 내용으로 답변",
|
||||
"cap": "단계가 많아 이제 정리하는 중",
|
||||
"noProgress": "새 데이터가 없어 정리하는 중"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "기억함: {fact}",
|
||||
"undo": "실행 취소",
|
||||
"accept": "네, 기억해 줘",
|
||||
"decline": "아니요",
|
||||
"confirmed": "알겠어요, 기억해 둘게요."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "이 계획 시작하기",
|
||||
"decline": "나중에",
|
||||
"confirmed": "기록했어요. {days}일 뒤에 함께 살펴볼게요."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "가정: {value}",
|
||||
"change": "변경"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "추천 답변"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, 값 {count}개",
|
||||
"labelFew": "{title}, {window}, 값 {count}개",
|
||||
"labelOther": "{title}, {window}, 값 {count}개"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} 대 {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "생각 깊이",
|
||||
"hint": "더 깊이 생각할수록 답변이 정확해지고 조금 더 기다리게 돼요.",
|
||||
"off": "끄기",
|
||||
"minimal": "최소",
|
||||
"low": "낮음",
|
||||
"medium": "중간",
|
||||
"high": "높음",
|
||||
"capped": "(관리자가 제한함)",
|
||||
"disabled": "관리자가 깊은 생각을 꺼 두었어요."
|
||||
},
|
||||
"memoryLink": "기억 ({count})"
|
||||
},
|
||||
"relativeJustNow": "방금",
|
||||
"relativeMinutesAgoOne": "{count}분 전",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "그 정보를 잊지 못했어요. 다시 시도해 주세요.",
|
||||
"clearedToast": "저장된 정보 {count}개를 잊었어요.",
|
||||
"clearError": "저장된 정보를 지우지 못했어요. 다시 시도해 주세요.",
|
||||
"detail": "코치는 이 정보로 답변을 상황에 맞게 유지해요. 잊었으면 하는 건 지우세요."
|
||||
"detail": "코치는 이 정보로 답변을 상황에 맞게 유지해요. 잊었으면 하는 건 지우세요.",
|
||||
"categoryMedication": "약"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "내 알림",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "코치를 켰어요.",
|
||||
"savedOff": "코치를 숨겼어요.",
|
||||
"saveError": "변경 사항을 저장하지 못했어요. 다시 시도해 주세요."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "편집",
|
||||
"source": {
|
||||
"user": "내가 알려 줌",
|
||||
"coach": "코치가 기억함"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7137,6 +7216,10 @@
|
||||
"documentAi": {
|
||||
"title": "문서 읽기",
|
||||
"description": "저장된 문서의 AI 읽기, 검사 결과지 스캔, 텍스트에서 약물 추출. 서버 밖으로 나가는 데이터의 대부분을 차지합니다."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "깊은 생각 허용",
|
||||
"max": "최대 생각 깊이"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "함께 보기: {metric}",
|
||||
"continue": "계속 찾아보기",
|
||||
"settingLabel": "후속 질문 추천",
|
||||
"settingHint": "코치의 가장 최근 답변 아래에 후속 질문을 최대 세 개까지 보여줘요."
|
||||
"settingHint": "코치의 가장 최근 답변 아래에 후속 질문을 최대 세 개까지 보여줘요.",
|
||||
"changeAssumption": "대신: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "코치가 질문이 있어요",
|
||||
|
||||
+87
-3
@@ -3146,7 +3146,78 @@
|
||||
"areasDoneOther": "Sprawdzono {count} obszarów",
|
||||
"info": "Szczegóły odpowiedzi",
|
||||
"infoModel": "Model: {model}"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"thinking": "Myśli…",
|
||||
"fetching": "Pobiera {domain}, {window}…",
|
||||
"digestOne": "Podsumowuje {count} pomiar…",
|
||||
"digestFew": "Podsumowuje {count} pomiary…",
|
||||
"digestOther": "Podsumowuje {count} pomiarów…",
|
||||
"digestDoneOne": "{count} pomiar z: {areas}",
|
||||
"digestDoneFew": "{count} pomiary z: {areas}",
|
||||
"digestDoneOther": "{count} pomiarów z: {areas}",
|
||||
"areasOne": "{count} obszar",
|
||||
"areasFew": "{count} obszary",
|
||||
"areasOther": "{count} obszarów",
|
||||
"memoryOne": "Pamięta {count} rzecz, którą mu powiedziałeś",
|
||||
"memoryFew": "Pamięta {count} rzeczy, które mu powiedziałeś",
|
||||
"memoryOther": "Pamięta {count} rzeczy, które mu powiedziałeś",
|
||||
"remember": "Zapamiętuje coś…",
|
||||
"plan": "Przygotowuje plan…",
|
||||
"asking": "Dopytuje…",
|
||||
"answer": "Pisze odpowiedź…",
|
||||
"summaryOne": "Przemyślane, {lookups} zapytanie, {seconds} s",
|
||||
"summaryFew": "Przemyślane, {lookups} zapytania, {seconds} s",
|
||||
"summaryOther": "Przemyślane, {lookups} zapytań, {seconds} s",
|
||||
"summaryNoLookups": "Przemyślane, {seconds} s",
|
||||
"stop": {
|
||||
"budget": "Budżet wyczerpany, odpowiada na podstawie tego, co ma",
|
||||
"time": "Limit czasu osiągnięty, odpowiada na podstawie tego, co ma",
|
||||
"cap": "Wiele kroków, teraz podsumowuje",
|
||||
"noProgress": "Brak nowych danych, podsumowuje"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"saved": "Zapamiętane: {fact}",
|
||||
"undo": "Cofnij",
|
||||
"accept": "Tak, zapamiętaj",
|
||||
"decline": "Nie",
|
||||
"confirmed": "Jasne, zapamiętam."
|
||||
},
|
||||
"plan": {
|
||||
"accept": "Przyjmij ten plan",
|
||||
"decline": "Nie teraz",
|
||||
"confirmed": "Zanotowane. Wrócimy do tego razem za {days} dni."
|
||||
},
|
||||
"assumption": {
|
||||
"line": "Założenie: {value}",
|
||||
"change": "Zmień"
|
||||
},
|
||||
"suggestedReplies": {
|
||||
"groupLabel": "Proponowane odpowiedzi"
|
||||
},
|
||||
"interim": {
|
||||
"labelOne": "{title}, {window}, {count} pomiar",
|
||||
"labelFew": "{title}, {window}, {count} pomiary",
|
||||
"labelOther": "{title}, {window}, {count} pomiarów"
|
||||
},
|
||||
"chart": {
|
||||
"compare": {
|
||||
"periods": "{a} wobec {b}"
|
||||
}
|
||||
},
|
||||
"reasoning": {
|
||||
"label": "Głębokość namysłu",
|
||||
"hint": "Głębszy namysł oznacza dokładniejsze odpowiedzi i trochę dłuższe czekanie.",
|
||||
"off": "Wyłączone",
|
||||
"minimal": "Minimalna",
|
||||
"low": "Niska",
|
||||
"medium": "Średnia",
|
||||
"high": "Wysoka",
|
||||
"capped": "(ograniczone przez administratora)",
|
||||
"disabled": "Administrator wyłączył głębszy namysł."
|
||||
},
|
||||
"memoryLink": "Pamięć ({count})"
|
||||
},
|
||||
"relativeJustNow": "przed chwilą",
|
||||
"relativeMinutesAgoOne": "{count} minutę temu",
|
||||
@@ -6008,7 +6079,8 @@
|
||||
"forgotError": "Nie udało się zapomnieć tej informacji. Spróbuj ponownie.",
|
||||
"clearedToast": "Zapomniano {count} zapisanych informacji.",
|
||||
"clearError": "Nie udało się usunąć zapisanych informacji. Spróbuj ponownie.",
|
||||
"detail": "Wykorzystuje je, aby jego odpowiedzi pozostawały trafne. Usuń wszystko, o czym wolisz, by zapomniał."
|
||||
"detail": "Wykorzystuje je, aby jego odpowiedzi pozostawały trafne. Usuń wszystko, o czym wolisz, by zapomniał.",
|
||||
"categoryMedication": "Leki"
|
||||
},
|
||||
"coachReminders": {
|
||||
"title": "Twoje przypomnienia",
|
||||
@@ -6663,6 +6735,13 @@
|
||||
"savedOn": "Coach włączony.",
|
||||
"savedOff": "Coach ukryty.",
|
||||
"saveError": "Nie udało się zapisać zmiany. Spróbuj ponownie."
|
||||
},
|
||||
"memory": {
|
||||
"edit": "Edytuj",
|
||||
"source": {
|
||||
"user": "od ciebie",
|
||||
"coach": "od Coacha"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationRedirectGuide": {
|
||||
@@ -7158,6 +7237,10 @@
|
||||
"documentAi": {
|
||||
"title": "Czytanie dokumentów",
|
||||
"description": "Czytanie zapisanych dokumentów przez AI, skanowanie wyników badań laboratoryjnych i odczytywanie leków z tekstu. Największa część danych opuszczających serwer."
|
||||
},
|
||||
"reasoning": {
|
||||
"enabled": "Zezwalaj na głębszy namysł",
|
||||
"max": "Największa głębokość namysłu"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
@@ -9933,7 +10016,8 @@
|
||||
"relatedMetric": "Sprawdź też: {metric}",
|
||||
"continue": "Szukaj dalej",
|
||||
"settingLabel": "Proponuj kolejne pytania",
|
||||
"settingHint": "Pokazuje do trzech propozycji pod ostatnią odpowiedzią Coacha."
|
||||
"settingHint": "Pokazuje do trzech propozycji pod ostatnią odpowiedzią Coacha.",
|
||||
"changeAssumption": "Zamiast tego: {value}"
|
||||
},
|
||||
"clarify": {
|
||||
"cardLabel": "Coach ma pytanie",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- The operator's reasoning controls on the instance row. Both columns carry a
|
||||
-- default, so the singleton row reads "reasoning allowed, no cap" the moment
|
||||
-- the migration lands and an upgrade needs no admin action.
|
||||
ALTER TABLE "app_settings"
|
||||
ADD COLUMN "ai_reasoning_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "ai_reasoning_max_effort" TEXT NOT NULL DEFAULT 'high';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- The model-written text of a Coach turn's live trail, encrypted like the
|
||||
-- message itself. Nullable and without a default: every existing turn has no
|
||||
-- trail, and adding a nullable column rewrites no rows.
|
||||
ALTER TABLE "coach_messages" ADD COLUMN "trail_encrypted" BYTEA;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Where a Coach fact came from, when it was last used in a turn, and the
|
||||
-- message it came out of. Every row written before this migration came from
|
||||
-- the background extraction or the deterministic matcher, which could not be
|
||||
-- told apart, so they all read as `extracted`.
|
||||
ALTER TABLE "coach_facts"
|
||||
ADD COLUMN "source" TEXT NOT NULL DEFAULT 'extracted',
|
||||
ADD COLUMN "last_used_at" TIMESTAMP(3),
|
||||
ADD COLUMN "source_message_id" TEXT;
|
||||
@@ -4950,6 +4950,17 @@ model AppSettings {
|
||||
assistantInsightStatusEnabled Boolean @default(true) @map("assistant_insight_status_enabled")
|
||||
assistantDocumentAiEnabled Boolean @default(true) @map("assistant_document_ai_enabled")
|
||||
|
||||
// v1.41 — the operator's reasoning controls. `aiReasoningEnabled` switches
|
||||
// provider-side reasoning off for every surface (the Coach and the three
|
||||
// background jobs that use it); `aiReasoningMaxEffort` caps the level a
|
||||
// person may pick (`low` | `medium` | `high`, an app-side closed enum in
|
||||
// `src/lib/ai/reasoning/levels.ts`). Resolved once per turn or job by
|
||||
// `src/lib/ai/reasoning/resolve.ts` and published per record on
|
||||
// `GET /api/auth/me` as `coachReasoning`. Defaults keep reasoning on with no
|
||||
// cap, so an upgrade needs no admin action.
|
||||
aiReasoningEnabled Boolean @default(true) @map("ai_reasoning_enabled")
|
||||
aiReasoningMaxEffort String @default("high") @map("ai_reasoning_max_effort")
|
||||
|
||||
// v1.18.0 — operator-level server-wide module availability. A DISABLED
|
||||
// allowlist mirroring the per-user `User.modulePreferencesJson`: absent /
|
||||
// empty / `true` ⇒ available, an explicit `false` ⇒ disabled for EVERY
|
||||
@@ -6435,6 +6446,13 @@ model CoachMessage {
|
||||
// only the tables' metadata. Read through `readMessageResults` by the
|
||||
// owner-narrowed results endpoint; NULL on every turn without a table.
|
||||
resultsEncrypted Bytes? @map("results_encrypted")
|
||||
// v1.41 — the model-written text of the turn's live trail (JSON
|
||||
// `CoachTrail`, at most 16 KB): reasoning titles and texts per round, the
|
||||
// facts recalled into the turn, the text of a fact proposal. AES-256-GCM
|
||||
// like `encryptedContent`. The trail's plaintext metadata (phase, status,
|
||||
// round, catalog label, duration, count, stop reason) rides
|
||||
// `metricSourceJson.activity`. Owner-only; NULL on every turn without one.
|
||||
trailEncrypted Bytes? @map("trail_encrypted")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
conversation CoachConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
@@ -6477,6 +6495,18 @@ model CoachFact {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
// Soft-delete: hides the fact from injection but keeps the row for audit.
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
// v1.41 — who put the fact here, an app-side closed enum
|
||||
// (`COACH_FACT_SOURCES` in `src/lib/ai/coach/memory/contract.ts`): `user`
|
||||
// (the remember button, or a proposal the person confirmed), `coach` (saved
|
||||
// during a turn), `extracted` (the background extraction, and every row
|
||||
// written before the column existed), `pattern` (the deterministic matcher).
|
||||
source String @default("extracted")
|
||||
// v1.41 — when the fact last went into a Coach turn; NULL until it has.
|
||||
lastUsedAt DateTime? @map("last_used_at")
|
||||
// v1.41 — the message the fact came out of, for "undo" and "from this
|
||||
// conversation". A bare id like `sourceConversationId`, not a relation, so
|
||||
// deleting a conversation does not take the fact with it.
|
||||
sourceMessageId String? @map("source_message_id")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
@@ -462,8 +462,8 @@ async function main() {
|
||||
}
|
||||
|
||||
// ───── Coach (Bytes columns) ─────
|
||||
// "encryptedContent" "resultsEncrypted" "summaryEncrypted" "titleEncrypted"
|
||||
// "factEncrypted"
|
||||
// "encryptedContent" "resultsEncrypted" "trailEncrypted" "summaryEncrypted"
|
||||
// "titleEncrypted" "factEncrypted"
|
||||
results.push(
|
||||
await rotateBytesColumn(
|
||||
"CoachMessage",
|
||||
@@ -480,6 +480,15 @@ async function main() {
|
||||
prisma.coachMessage,
|
||||
),
|
||||
);
|
||||
// v1.41 — the model text of the turn's trail. NULL on every turn without
|
||||
// one, which `rotateBytesColumn` skips.
|
||||
results.push(
|
||||
await rotateBytesColumn(
|
||||
"CoachMessage",
|
||||
"trailEncrypted",
|
||||
prisma.coachMessage,
|
||||
),
|
||||
);
|
||||
results.push(
|
||||
await rotateBytesColumn(
|
||||
"CoachConversation",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
* shape would make the whole frame fail to decode, and a new frame that
|
||||
* reused one with the same shape would leak a value into whatever the client
|
||||
* does with that key. So a new frame names its payload with a key of its own
|
||||
* (`step`, `result`, `followUps`, `clarification`).
|
||||
* (`step`, `result`, `followUps`, `clarification`, and since v1.41
|
||||
* `activity`, `memoryNote` with `note`, `planProposal` with `proposal`).
|
||||
*
|
||||
* Reads the frame list from the Zod mirror (`stream-events.ts`), which a
|
||||
* type test holds equal to `CoachStreamEvent`, so a frame added to the
|
||||
@@ -81,6 +82,9 @@ describe("Coach stream frames stay decodable by the shipped native client", () =
|
||||
"result",
|
||||
"followUps",
|
||||
"clarification",
|
||||
"activity",
|
||||
"memoryNote",
|
||||
"planProposal",
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -122,6 +122,9 @@ export function CoachResultChart({
|
||||
if (spec.kind === "histogram") {
|
||||
return <HistogramChart result={result} spec={spec} label={label} />;
|
||||
}
|
||||
// v1.41 — the contract is in place; no table carries this kind yet, and the
|
||||
// table view stays available until the chart lands.
|
||||
if (spec.kind === "compare") return null;
|
||||
if (spec.kind === "bar" && spec.orientation === "horizontal") {
|
||||
return (
|
||||
<CategoryBarChart
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* The v1.41 message keys and the seven bundles agree, both ways.
|
||||
*
|
||||
* The trail, memory, plan, reasoning and reply keys were laid down before
|
||||
* the code that renders them, so the catalogs in the contract modules are
|
||||
* their only reference until then. Forward: every key a catalog can produce
|
||||
* resolves in every locale, with the same placeholders as English. Reverse:
|
||||
* every leaf under the namespaces these catalogs own is one they can
|
||||
* produce, so a string cannot sit in the bundles with nothing able to reach
|
||||
* it.
|
||||
*
|
||||
* Checked by breaking it: deleting `insights.coach.activity.answer` from
|
||||
* `pl.json` fails the forward test by locale and key; renaming `{fact}` in
|
||||
* the German `insights.coach.memory.saved` fails the placeholder test; adding
|
||||
* an unused leaf under `insights.coach.reasoning` fails the reverse test.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
COACH_ACTIVITY_KEYS,
|
||||
COACH_ACTIVITY_STOP_KEYS,
|
||||
activityAreasKey,
|
||||
activityDigestDoneKey,
|
||||
activityDigestKey,
|
||||
activityMemoryKey,
|
||||
activitySummaryKey,
|
||||
} from "@/lib/ai/coach/activity/contract";
|
||||
import {
|
||||
COACH_ASSUMPTION_KEYS,
|
||||
COACH_CHART_COMPARE_KEYS,
|
||||
COACH_SUGGESTED_REPLIES_KEYS,
|
||||
interimLabelKey,
|
||||
} from "@/lib/ai/coach/dialog-keys";
|
||||
import {
|
||||
COACH_MEMORY_KEYS,
|
||||
COACH_MEMORY_LIST_KEYS,
|
||||
COACH_PLAN_KEYS,
|
||||
} from "@/lib/ai/coach/memory/contract";
|
||||
import {
|
||||
REASONING_ADMIN_KEYS,
|
||||
REASONING_LEVEL_LABEL_KEYS,
|
||||
REASONING_SETTING_KEYS,
|
||||
} from "@/lib/ai/reasoning/levels";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
const ROOT = join(__dirname, "../../../../..");
|
||||
|
||||
function strings(locale: Locale): Map<string, unknown> {
|
||||
const out = new Map<string, unknown>();
|
||||
const walk = (node: unknown, prefix: string) => {
|
||||
if (node && typeof node === "object") {
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
walk(value, prefix ? `${prefix}.${key}` : key);
|
||||
}
|
||||
} else {
|
||||
out.set(prefix, node);
|
||||
}
|
||||
};
|
||||
walk(
|
||||
JSON.parse(
|
||||
readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8"),
|
||||
) as unknown,
|
||||
"",
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Counts that land on every plural tier some locale uses. */
|
||||
const COUNTS = [0, 1, 2, 3, 5, 12, 22];
|
||||
|
||||
function catalogKeys(): Set<string> {
|
||||
const keys = new Set<string>([
|
||||
...Object.values(COACH_ACTIVITY_KEYS),
|
||||
...Object.values(COACH_ACTIVITY_STOP_KEYS),
|
||||
...Object.values(COACH_MEMORY_KEYS),
|
||||
...Object.values(COACH_PLAN_KEYS),
|
||||
...Object.values(COACH_MEMORY_LIST_KEYS),
|
||||
...Object.values(COACH_ASSUMPTION_KEYS),
|
||||
...Object.values(COACH_SUGGESTED_REPLIES_KEYS),
|
||||
...Object.values(COACH_CHART_COMPARE_KEYS),
|
||||
...Object.values(REASONING_LEVEL_LABEL_KEYS),
|
||||
...Object.values(REASONING_SETTING_KEYS),
|
||||
...Object.values(REASONING_ADMIN_KEYS),
|
||||
]);
|
||||
for (const locale of locales) {
|
||||
for (const count of COUNTS) {
|
||||
keys.add(activityDigestKey(count, locale));
|
||||
keys.add(activityDigestDoneKey(count, locale));
|
||||
keys.add(activityAreasKey(count, locale));
|
||||
keys.add(activityMemoryKey(count, locale));
|
||||
keys.add(activitySummaryKey(count, locale));
|
||||
keys.add(interimLabelKey(count, locale));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
const OWNED = [
|
||||
"insights.coach.activity",
|
||||
"insights.coach.memory",
|
||||
"insights.coach.plan",
|
||||
"insights.coach.assumption",
|
||||
"insights.coach.suggestedReplies",
|
||||
"insights.coach.interim",
|
||||
"insights.coach.chart",
|
||||
"insights.coach.reasoning",
|
||||
"settings.coach.memory",
|
||||
"admin.assistant.reasoning",
|
||||
];
|
||||
|
||||
const placeholders = (text: string) =>
|
||||
[...text.matchAll(/\{(\w+)\}/g)].map((match) => match[1]).sort();
|
||||
|
||||
describe("v1.41 contract keys", () => {
|
||||
const keys = catalogKeys();
|
||||
const en = strings("en");
|
||||
|
||||
it("produces the whole catalog", () => {
|
||||
// Every key laid down for the release but the `change_assumption` chip
|
||||
// label, which the dialog catalog's own test covers.
|
||||
expect(keys.size).toBe(57);
|
||||
});
|
||||
|
||||
it.each(locales)("every catalog key resolves in %s", (locale) => {
|
||||
const bundle = strings(locale);
|
||||
const missing = [...keys].filter(
|
||||
(key) => typeof bundle.get(key) !== "string" || bundle.get(key) === "",
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(locales)("%s keeps the English placeholders", (locale) => {
|
||||
const bundle = strings(locale);
|
||||
const drifted = [...keys].filter(
|
||||
(key) =>
|
||||
placeholders(String(bundle.get(key))).join() !==
|
||||
placeholders(String(en.get(key))).join(),
|
||||
);
|
||||
expect(drifted).toEqual([]);
|
||||
});
|
||||
|
||||
it("every leaf under the owned namespaces is reachable from a catalog", () => {
|
||||
const owned = [...en.keys()].filter((key) =>
|
||||
OWNED.some((ns) => key.startsWith(`${ns}.`)),
|
||||
);
|
||||
expect(owned.length).toBeGreaterThan(40);
|
||||
expect(owned.filter((key) => !keys.has(key))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,15 @@
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ACTIVITY_MAX_ENTRIES } from "../activity/contract";
|
||||
import {
|
||||
coachActivityMetaSchema,
|
||||
coachClarificationSchema,
|
||||
coachFollowUpSchema,
|
||||
coachMemoryNoteMetaSchema,
|
||||
coachResultMetaSchema,
|
||||
coachStepSchema,
|
||||
coachTrailSchema,
|
||||
} from "../stream-events";
|
||||
|
||||
const STEP = {
|
||||
@@ -107,3 +111,86 @@ describe("stored dialog ids", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v1.41 ids", () => {
|
||||
const ACTIVITY = {
|
||||
id: "a1",
|
||||
phase: "fetch",
|
||||
status: "done",
|
||||
round: 1,
|
||||
labelKey: "insights.coach.activity.fetching",
|
||||
label: "Fetching blood pressure, last 30 days…",
|
||||
stepRef: "s7",
|
||||
};
|
||||
|
||||
it("takes eight tables a message and no ninth", () => {
|
||||
expect(
|
||||
coachResultMetaSchema.safeParse({ ...META, ref: "r8" }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
coachResultMetaSchema.safeParse({ ...META, ref: "r9" }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachFollowUpSchema.safeParse({
|
||||
...CHIP,
|
||||
anchor: { ref: "r8", domain: "bp" },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("numbers trail entries a1 up to the cap", () => {
|
||||
expect(coachActivityMetaSchema.safeParse(ACTIVITY).success).toBe(true);
|
||||
expect(
|
||||
coachActivityMetaSchema.safeParse({
|
||||
...ACTIVITY,
|
||||
id: `a${ACTIVITY_MAX_ENTRIES}`,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
coachActivityMetaSchema.safeParse({
|
||||
...ACTIVITY,
|
||||
id: `a${ACTIVITY_MAX_ENTRIES + 1}`,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses anything that could carry text into a prompt", () => {
|
||||
expect(
|
||||
coachActivityMetaSchema.safeParse({ ...ACTIVITY, id: PLANT }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachActivityMetaSchema.safeParse({ ...ACTIVITY, stepRef: PLANT })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachTrailSchema.safeParse({ entries: [{ id: PLANT, title: "t" }] })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachMemoryNoteMetaSchema.safeParse({
|
||||
proposal: false,
|
||||
factId: PLANT,
|
||||
category: "goal",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachClarificationSchema.safeParse({
|
||||
kind: "goal",
|
||||
freeText: true,
|
||||
assumption: PLANT,
|
||||
choices: [
|
||||
{ id: "c1", labelKey: "k", label: "l", value: { goal: "plan1" } },
|
||||
],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
coachClarificationSchema.safeParse({
|
||||
kind: "goal",
|
||||
freeText: true,
|
||||
choices: [
|
||||
{ id: "c1", labelKey: "k", label: "l", value: { goal: PLANT } },
|
||||
],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -15,26 +15,44 @@ import { describe, expectTypeOf, it } from "vitest";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import type {
|
||||
CoachActivity,
|
||||
CoachActivityMeta,
|
||||
CoachAssumption,
|
||||
CoachClarification,
|
||||
CoachFollowUp,
|
||||
CoachMemoryNote,
|
||||
CoachMemoryNoteMeta,
|
||||
CoachPlanProposal,
|
||||
CoachPlanProposalMeta,
|
||||
CoachMethod,
|
||||
CoachProvenance,
|
||||
CoachResultEntry,
|
||||
CoachResultMeta,
|
||||
CoachResultTable,
|
||||
CoachStep,
|
||||
CoachStop,
|
||||
CoachStreamEvent,
|
||||
CoachTrail,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import {
|
||||
coachActivityMetaSchema,
|
||||
coachActivitySchema,
|
||||
coachAssumptionSchema,
|
||||
coachClarificationSchema,
|
||||
coachFollowUpSchema,
|
||||
coachMemoryNoteMetaSchema,
|
||||
coachMemoryNoteSchema,
|
||||
coachPlanProposalMetaSchema,
|
||||
coachPlanProposalSchema,
|
||||
coachMethodSchema,
|
||||
coachProvenanceSchema,
|
||||
coachResultEntrySchema,
|
||||
coachResultMetaSchema,
|
||||
coachResultTableSchema,
|
||||
coachStepSchema,
|
||||
coachStopSchema,
|
||||
coachStreamEventSchema,
|
||||
coachTrailSchema,
|
||||
} from "@/lib/ai/coach/stream-events";
|
||||
|
||||
describe("Coach wire: Zod mirror equals the TypeScript contract", () => {
|
||||
@@ -68,4 +86,32 @@ describe("Coach wire: Zod mirror equals the TypeScript contract", () => {
|
||||
z.infer<typeof coachProvenanceSchema>
|
||||
>().toEqualTypeOf<CoachProvenance>();
|
||||
});
|
||||
|
||||
it("the v1.41 trail, memory and plan shapes", () => {
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachActivitySchema>
|
||||
>().toEqualTypeOf<CoachActivity>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachActivityMetaSchema>
|
||||
>().toEqualTypeOf<CoachActivityMeta>();
|
||||
expectTypeOf<z.infer<typeof coachStopSchema>>().toEqualTypeOf<CoachStop>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachAssumptionSchema>
|
||||
>().toEqualTypeOf<CoachAssumption>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachMemoryNoteSchema>
|
||||
>().toEqualTypeOf<CoachMemoryNote>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachMemoryNoteMetaSchema>
|
||||
>().toEqualTypeOf<CoachMemoryNoteMeta>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachPlanProposalSchema>
|
||||
>().toEqualTypeOf<CoachPlanProposal>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachPlanProposalMetaSchema>
|
||||
>().toEqualTypeOf<CoachPlanProposalMeta>();
|
||||
expectTypeOf<
|
||||
z.infer<typeof coachTrailSchema>
|
||||
>().toEqualTypeOf<CoachTrail>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ACTIVITY_TEXT_MAX_CHARS,
|
||||
ACTIVITY_TITLE_MAX_CHARS,
|
||||
TRAIL_MAX_BYTES,
|
||||
activitySummaryKey,
|
||||
createNoopActivityRecorder,
|
||||
} from "../contract";
|
||||
|
||||
describe("activity contract", () => {
|
||||
it("the no-op recorder sends and stores nothing", () => {
|
||||
const recorder = createNoopActivityRecorder();
|
||||
const id = recorder.start({
|
||||
phase: "thinking",
|
||||
round: 1,
|
||||
labelKey: "insights.coach.activity.thinking",
|
||||
label: "Thinking…",
|
||||
});
|
||||
recorder.update(id, { title: "**Checking the trend**" });
|
||||
recorder.finish(id, "done", { text: "Looked at the last month." });
|
||||
expect(recorder.meta()).toEqual([]);
|
||||
expect(recorder.trail()).toBeNull();
|
||||
});
|
||||
|
||||
it("a full trail of model text fits its stored bound", () => {
|
||||
// 99 entries can never all carry text: the bound is what a turn stores,
|
||||
// and the recorder trims to it. A dozen rounds of full text must fit.
|
||||
const twelveRounds = JSON.stringify({
|
||||
entries: Array.from({ length: 12 }, (_, i) => ({
|
||||
id: `a${i + 1}`,
|
||||
title: "t".repeat(ACTIVITY_TITLE_MAX_CHARS),
|
||||
text: "x".repeat(ACTIVITY_TEXT_MAX_CHARS),
|
||||
})),
|
||||
});
|
||||
expect(new TextEncoder().encode(twelveRounds).byteLength).toBeLessThan(
|
||||
TRAIL_MAX_BYTES,
|
||||
);
|
||||
});
|
||||
|
||||
it("says nothing about lookups on a turn that made none", () => {
|
||||
expect(activitySummaryKey(0, "en")).toBe(
|
||||
"insights.coach.activity.summaryNoLookups",
|
||||
);
|
||||
expect(activitySummaryKey(1, "en")).toBe(
|
||||
"insights.coach.activity.summaryOne",
|
||||
);
|
||||
expect(activitySummaryKey(3, "pl")).toBe(
|
||||
"insights.coach.activity.summaryFew",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* v1.41 — the contract of a turn's live trail.
|
||||
*
|
||||
* The loop reports what it is doing through an {@link ActivityRecorder}: it
|
||||
* opens an entry before each phase starts (the thinking round before the
|
||||
* provider call, each fetch before its tool runs), updates it while it runs
|
||||
* (a reasoning title arriving, a count) and closes it. The recorder sends the
|
||||
* `activity` frames, screens model text before it goes anywhere, and at the
|
||||
* end hands back the plaintext metadata for `metricSource.activity` and the
|
||||
* model text for `coach_messages.trail_encrypted`.
|
||||
*
|
||||
* Labels are always catalog text rendered on the server; model text only ever
|
||||
* rides `title` and `text`. A `checkpoint` entry (assistant text that came
|
||||
* beside tool calls) is labelled with the thinking key and carries the
|
||||
* screened sentence as its title.
|
||||
*
|
||||
* The no-op recorder is what every caller holds until the real one is wired:
|
||||
* it sends nothing and stores nothing, so a turn behaves exactly as before.
|
||||
*
|
||||
* Client-safe: no server import.
|
||||
*/
|
||||
import type {
|
||||
CoachActivityMeta,
|
||||
CoachActivityPhase,
|
||||
CoachStepStatus,
|
||||
CoachStopReason,
|
||||
CoachTrail,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { pluralKey } from "@/lib/i18n/plural";
|
||||
|
||||
/** The stored trail text of one message, as UTF-8 JSON, before encryption. */
|
||||
export const TRAIL_MAX_BYTES = 16 * 1024;
|
||||
|
||||
/** A reasoning title or checkpoint sentence, after screening. */
|
||||
export const ACTIVITY_TITLE_MAX_CHARS = 80;
|
||||
|
||||
/** A reasoning summary per round, after screening. */
|
||||
export const ACTIVITY_TEXT_MAX_CHARS = 400;
|
||||
|
||||
/** `a1`..`a99`: the 100th entry of a turn is not recorded. */
|
||||
export const ACTIVITY_MAX_ENTRIES = 99;
|
||||
|
||||
export interface ActivityStart {
|
||||
phase: CoachActivityPhase;
|
||||
round: number;
|
||||
labelKey: string;
|
||||
label: string;
|
||||
stepRef?: string;
|
||||
count?: number;
|
||||
stop?: CoachStopReason;
|
||||
}
|
||||
|
||||
export interface ActivityPatch {
|
||||
/** Model text; screened by the recorder, dropped when it fails. */
|
||||
title?: string;
|
||||
/** Model text; screened by the recorder, dropped when it fails. */
|
||||
text?: string;
|
||||
/** A new catalog label (the digest count once the round has settled). */
|
||||
labelKey?: string;
|
||||
label?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface ActivityRecorder {
|
||||
/** Opens an entry, sends its `running` frame and returns its id. */
|
||||
start(entry: ActivityStart): string;
|
||||
/** Updates an open entry and sends the frame again. */
|
||||
update(id: string, patch: ActivityPatch): void;
|
||||
/** Closes an entry with its final status and duration. */
|
||||
finish(
|
||||
id: string,
|
||||
status: Exclude<CoachStepStatus, "running">,
|
||||
patch?: ActivityPatch,
|
||||
): void;
|
||||
/** The entries so far, for `metricSource.activity`. */
|
||||
meta(): CoachActivityMeta[];
|
||||
/** The screened model text so far, or null when there is none. */
|
||||
trail(): CoachTrail | null;
|
||||
}
|
||||
|
||||
/** Sends nothing, stores nothing: a turn runs exactly as it did before. */
|
||||
export function createNoopActivityRecorder(): ActivityRecorder {
|
||||
return {
|
||||
start: () => "a1",
|
||||
update: () => undefined,
|
||||
finish: () => undefined,
|
||||
meta: () => [],
|
||||
trail: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Message keys ────────────────────────────────────────────────────────────
|
||||
|
||||
/** The labels with no count. */
|
||||
export const COACH_ACTIVITY_KEYS = {
|
||||
thinking: "insights.coach.activity.thinking",
|
||||
fetching: "insights.coach.activity.fetching",
|
||||
remember: "insights.coach.activity.remember",
|
||||
plan: "insights.coach.activity.plan",
|
||||
asking: "insights.coach.activity.asking",
|
||||
answer: "insights.coach.activity.answer",
|
||||
} as const;
|
||||
|
||||
/** The `stop` entry's label, by reason. */
|
||||
export const COACH_ACTIVITY_STOP_KEYS: Readonly<
|
||||
Record<CoachStopReason, string>
|
||||
> = {
|
||||
budget: "insights.coach.activity.stop.budget",
|
||||
time: "insights.coach.activity.stop.time",
|
||||
cap: "insights.coach.activity.stop.cap",
|
||||
no_progress: "insights.coach.activity.stop.noProgress",
|
||||
};
|
||||
|
||||
/** "Summarising N readings…", while a round's results are folded in. */
|
||||
export function activityDigestKey(count: number, locale: Locale): string {
|
||||
return pluralKey("insights.coach.activity.digest", count, locale);
|
||||
}
|
||||
|
||||
/** "N readings from {areas}", once the round has settled. */
|
||||
export function activityDigestDoneKey(count: number, locale: Locale): string {
|
||||
return pluralKey("insights.coach.activity.digestDone", count, locale);
|
||||
}
|
||||
|
||||
/** "N areas", the `{areas}` of the settled digest. */
|
||||
export function activityAreasKey(count: number, locale: Locale): string {
|
||||
return pluralKey("insights.coach.activity.areas", count, locale);
|
||||
}
|
||||
|
||||
/** "Recalls N things you told it". */
|
||||
export function activityMemoryKey(count: number, locale: Locale): string {
|
||||
return pluralKey("insights.coach.activity.memory", count, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* The one-line summary once the turn is done: "Thought it through, N lookups,
|
||||
* S s", or the seconds alone on a turn that looked nothing up.
|
||||
*/
|
||||
export function activitySummaryKey(lookups: number, locale: Locale): string {
|
||||
return lookups === 0
|
||||
? "insights.coach.activity.summaryNoLookups"
|
||||
: pluralKey("insights.coach.activity.summary", lookups, locale);
|
||||
}
|
||||
@@ -231,6 +231,7 @@ export const COACH_FOLLOW_UP_KEYS: Readonly<Record<CoachFollowUpKind, string>> =
|
||||
as_table: "coach.followUp.asTable",
|
||||
related_metric: "coach.followUp.relatedMetric",
|
||||
continue: "coach.followUp.continue",
|
||||
change_assumption: "coach.followUp.changeAssumption",
|
||||
};
|
||||
|
||||
export const COACH_FOLLOW_UP_UI_KEYS = {
|
||||
@@ -253,6 +254,29 @@ export function clarifyWindowLabelKey(window: CoachScopeWindow): string {
|
||||
return `coach.clarify.window.${window}`;
|
||||
}
|
||||
|
||||
// ── Replies, assumptions, interim tables (v1.41) ──────────────────────────
|
||||
|
||||
/** The group of reply pills under the latest answer. */
|
||||
export const COACH_SUGGESTED_REPLIES_KEYS = {
|
||||
groupLabel: "insights.coach.suggestedReplies.groupLabel",
|
||||
} as const;
|
||||
|
||||
/** "Assumed: last 30 days. Change". */
|
||||
export const COACH_ASSUMPTION_KEYS = {
|
||||
line: "insights.coach.assumption.line",
|
||||
change: "insights.coach.assumption.change",
|
||||
} as const;
|
||||
|
||||
/** The one-line preview of a table read while the turn still runs. */
|
||||
export function interimLabelKey(count: number, locale: Locale): string {
|
||||
return pluralKey("insights.coach.interim.label", count, locale);
|
||||
}
|
||||
|
||||
/** The legend of a chart comparing two periods ("{a} vs {b}"). */
|
||||
export const COACH_CHART_COMPARE_KEYS = {
|
||||
periods: "insights.coach.chart.compare.periods",
|
||||
} as const;
|
||||
|
||||
// ── Reuse ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** The prose of a turn answered from a stored table without a model call. */
|
||||
|
||||
@@ -129,6 +129,9 @@ export function followUpContextHint(
|
||||
case "continue":
|
||||
// Answered with its own context lines (`continue.ts`).
|
||||
return null;
|
||||
case "change_assumption":
|
||||
// v1.41 — the contract is in place; no chip of this kind is offered yet.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { COACH_MEMORY_CATEGORIES } from "@/lib/ai/coach/types";
|
||||
|
||||
import {
|
||||
COACH_FACT_SOURCES,
|
||||
HEALTH_MEMORY_CATEGORIES,
|
||||
MEMORY_BLOCK_MAX_CHARS,
|
||||
buildMemoryContextBlock,
|
||||
buildPlanProgressLines,
|
||||
decideFactProposal,
|
||||
decidePlanProposal,
|
||||
proposePlanFromTool,
|
||||
rememberFactFromTool,
|
||||
} from "../contract";
|
||||
|
||||
const IDS = { userId: "u1", conversationId: "c1" };
|
||||
|
||||
describe("memory contract stubs", () => {
|
||||
it("build no block, write nothing and decide nothing", async () => {
|
||||
await expect(
|
||||
buildMemoryContextBlock({ ...IDS, locale: "en" }),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
rememberFactFromTool({
|
||||
...IDS,
|
||||
userMessage: "I want to reach 75 kg by December",
|
||||
call: { category: "goal", fact: "Wants 75 kg by December", why: "w" },
|
||||
}),
|
||||
).resolves.toEqual({ kind: "declined", reason: "unavailable" });
|
||||
await expect(
|
||||
decideFactProposal({
|
||||
...IDS,
|
||||
messageId: "m1",
|
||||
proposalId: "p1",
|
||||
accept: true,
|
||||
}),
|
||||
).resolves.toEqual({ kind: "stale" });
|
||||
await expect(
|
||||
proposePlanFromTool({
|
||||
...IDS,
|
||||
call: {
|
||||
metric: "WEIGHT",
|
||||
ifCue: "after dinner",
|
||||
thenAction: "walk",
|
||||
reviewInDays: 14,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({ kind: "declined", reason: "unavailable" });
|
||||
await expect(
|
||||
decidePlanProposal({
|
||||
...IDS,
|
||||
messageId: "m1",
|
||||
planId: "p1",
|
||||
accept: true,
|
||||
}),
|
||||
).resolves.toEqual({ kind: "stale" });
|
||||
await expect(buildPlanProgressLines("u1")).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory contract constants", () => {
|
||||
it("health categories are a subset of the memory categories", () => {
|
||||
for (const category of HEALTH_MEMORY_CATEGORIES) {
|
||||
expect(COACH_MEMORY_CATEGORIES).toContain(category);
|
||||
}
|
||||
expect([...HEALTH_MEMORY_CATEGORIES].sort()).toEqual([
|
||||
"condition",
|
||||
"constraint",
|
||||
"medication",
|
||||
]);
|
||||
});
|
||||
|
||||
it("the column default is one of the sources", () => {
|
||||
const schema = readFileSync(
|
||||
join(__dirname, "../../../../../../prisma/schema.prisma"),
|
||||
"utf8",
|
||||
);
|
||||
const model = schema.slice(schema.indexOf("model CoachFact {"));
|
||||
const match = /source\s+String\s+@default\("(\w+)"\)/.exec(model);
|
||||
expect(match?.[1]).toBeDefined();
|
||||
expect(COACH_FACT_SOURCES).toContain(match?.[1]);
|
||||
});
|
||||
|
||||
it("keeps the memory block small next to the inventory", () => {
|
||||
expect(MEMORY_BLOCK_MAX_CHARS).toBeLessThanOrEqual(1_500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* v1.41 — the contract of the Coach's memory inside a turn.
|
||||
*
|
||||
* The loop and the chat route reach facts and plans only through these
|
||||
* functions: the memory block that goes into every turn, the `remember_fact`
|
||||
* and `propose_plan` tools, the two decisions a person answers with a tap
|
||||
* (`memoryDecision`, `planDecision` on the chat request), and the progress
|
||||
* lines the daily briefing quotes.
|
||||
*
|
||||
* Until the implementations land, every function is a stub that answers "not
|
||||
* available" and touches nothing: no block is built, no fact is written, no
|
||||
* plan changes state. A turn behaves exactly as it did before.
|
||||
*
|
||||
* Rules every implementation keeps, so a caller can rely on them:
|
||||
* - health facts (`condition`, `constraint`, `medication`) are only ever
|
||||
* proposed, never saved without the person's tap;
|
||||
* - a fact comes from the person's current message, never from a document or
|
||||
* a quote;
|
||||
* - the block is built only after the turn has passed its egress check, and
|
||||
* never for a read-only sharing viewer;
|
||||
* - fact and plan text never reaches a log or `annotate()`; counts and ids do.
|
||||
*/
|
||||
import type {
|
||||
CoachMemoryCategory,
|
||||
CoachMemoryNote,
|
||||
CoachPlanProposal,
|
||||
} from "@/lib/ai/coach/types";
|
||||
|
||||
/** Who put a fact into `coach_facts.source`. */
|
||||
export const COACH_FACT_SOURCES = [
|
||||
"user",
|
||||
"coach",
|
||||
"extracted",
|
||||
"pattern",
|
||||
] as const;
|
||||
|
||||
/** The categories that are proposed and never saved without a tap. */
|
||||
export const HEALTH_MEMORY_CATEGORIES: ReadonlySet<CoachMemoryCategory> =
|
||||
new Set(["condition", "constraint", "medication"]);
|
||||
|
||||
/** A remembered fact, as `remember_fact` may write it. */
|
||||
export const REMEMBER_FACT_MAX_CHARS = 160;
|
||||
|
||||
/** The memory block in a turn's context, never trimmed before the inventory. */
|
||||
export const MEMORY_BLOCK_MAX_CHARS = 1_500;
|
||||
|
||||
/** A proposed plan's review window, in days. */
|
||||
export const PLAN_REVIEW_DAYS = { min: 7, max: 56 } as const;
|
||||
|
||||
// ── The memory block ───────────────────────────────────────────────────────
|
||||
|
||||
export interface MemoryContextBlock {
|
||||
/** The fenced `WHAT YOU KNOW ABOUT THIS PERSON` section; data, not orders. */
|
||||
text: string;
|
||||
/** The facts in the block, for `last_used_at` and the `memory` entry. */
|
||||
factIds: string[];
|
||||
planIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The facts and active plans a turn starts with, or null when there are none
|
||||
* or memory is unavailable. The caller has already passed the egress check.
|
||||
*/
|
||||
export async function buildMemoryContextBlock(_args: {
|
||||
userId: string;
|
||||
conversationId: string | undefined;
|
||||
locale: string;
|
||||
}): Promise<MemoryContextBlock | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── remember_fact ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface RememberFactCall {
|
||||
category: CoachMemoryCategory;
|
||||
/** At most {@link REMEMBER_FACT_MAX_CHARS}. */
|
||||
fact: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
export type RememberFactOutcome =
|
||||
| { kind: "saved"; note: CoachMemoryNote }
|
||||
| { kind: "proposed"; note: CoachMemoryNote }
|
||||
| {
|
||||
kind: "declined";
|
||||
reason:
|
||||
| "duplicate"
|
||||
| "not_from_message"
|
||||
| "one_per_answer"
|
||||
| "invalid"
|
||||
| "unavailable";
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a `remember_fact` call. `userMessage` is the person's current message,
|
||||
* the only text a fact may come from.
|
||||
*/
|
||||
export async function rememberFactFromTool(_args: {
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
userMessage: string;
|
||||
call: RememberFactCall;
|
||||
}): Promise<RememberFactOutcome> {
|
||||
return { kind: "declined", reason: "unavailable" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a fact proposal. The fact is read from the trail stored on
|
||||
* `messageId`, never from the request.
|
||||
*/
|
||||
export async function decideFactProposal(_args: {
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
proposalId: string;
|
||||
accept: boolean;
|
||||
}): Promise<
|
||||
{ kind: "saved"; factId: string } | { kind: "declined" } | { kind: "stale" }
|
||||
> {
|
||||
return { kind: "stale" };
|
||||
}
|
||||
|
||||
// ── propose_plan ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ProposePlanCall {
|
||||
metric: string;
|
||||
target?: string;
|
||||
ifCue: string;
|
||||
thenAction: string;
|
||||
/** {@link PLAN_REVIEW_DAYS}. */
|
||||
reviewInDays: number;
|
||||
}
|
||||
|
||||
export type ProposePlanOutcome =
|
||||
| { kind: "proposed"; proposal: CoachPlanProposal }
|
||||
| {
|
||||
kind: "declined";
|
||||
reason: "one_per_answer" | "too_many_open" | "invalid" | "unavailable";
|
||||
};
|
||||
|
||||
/** Runs a `propose_plan` call: writes the plan as `proposed`. */
|
||||
export async function proposePlanFromTool(_args: {
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
call: ProposePlanCall;
|
||||
}): Promise<ProposePlanOutcome> {
|
||||
return { kind: "declined", reason: "unavailable" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a plan proposal: `active` on accept, `abandoned` on decline. Only a
|
||||
* plan the assistant message proposed, and only the person's own.
|
||||
*/
|
||||
export async function decidePlanProposal(_args: {
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
planId: string;
|
||||
accept: boolean;
|
||||
}): Promise<
|
||||
| { kind: "activated"; reviewInDays: number }
|
||||
| { kind: "abandoned" }
|
||||
| { kind: "stale" }
|
||||
> {
|
||||
return { kind: "stale" };
|
||||
}
|
||||
|
||||
// ── The briefing ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* One server-computed progress sentence per active plan, at most two, for
|
||||
* the daily briefing prompt. The model may quote one; it may not invent one.
|
||||
*/
|
||||
export async function buildPlanProgressLines(
|
||||
_userId: string,
|
||||
): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── Message keys ────────────────────────────────────────────────────────────
|
||||
|
||||
/** The memory note under an answer, and its two replies. */
|
||||
export const COACH_MEMORY_KEYS = {
|
||||
saved: "insights.coach.memory.saved",
|
||||
undo: "insights.coach.memory.undo",
|
||||
accept: "insights.coach.memory.accept",
|
||||
decline: "insights.coach.memory.decline",
|
||||
confirmed: "insights.coach.memory.confirmed",
|
||||
} as const;
|
||||
|
||||
/** A plan proposal's two replies, and the line a decision turn answers with. */
|
||||
export const COACH_PLAN_KEYS = {
|
||||
accept: "insights.coach.plan.accept",
|
||||
decline: "insights.coach.plan.decline",
|
||||
confirmed: "insights.coach.plan.confirmed",
|
||||
} as const;
|
||||
|
||||
/** The memory list in settings, and the link to it from the Coach. */
|
||||
export const COACH_MEMORY_LIST_KEYS = {
|
||||
link: "insights.coach.memoryLink",
|
||||
edit: "settings.coach.memory.edit",
|
||||
sourceUser: "settings.coach.memory.source.user",
|
||||
sourceCoach: "settings.coach.memory.source.coach",
|
||||
categoryMedication: "settings.ai.coachMemory.categoryMedication",
|
||||
} as const;
|
||||
@@ -14,12 +14,21 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { AI_UNAVAILABLE_REASONS } from "@/lib/ai/capabilities/types";
|
||||
import {
|
||||
ACTIVITY_TEXT_MAX_CHARS,
|
||||
ACTIVITY_TITLE_MAX_CHARS,
|
||||
} from "@/lib/ai/coach/activity/contract";
|
||||
import {
|
||||
PLAN_REVIEW_DAYS,
|
||||
REMEMBER_FACT_MAX_CHARS,
|
||||
} from "@/lib/ai/coach/memory/contract";
|
||||
import {
|
||||
SUGGESTED_ACTION_TYPES,
|
||||
type CheckupIntervalId,
|
||||
} from "@/lib/ai/coach/suggest-action";
|
||||
import { COACH_TOOL_NAMES } from "@/lib/ai/coach/tools/definitions";
|
||||
import {
|
||||
COACH_MEMORY_CATEGORIES,
|
||||
coachKeyValueSchema,
|
||||
coachScopeSourceSchema,
|
||||
coachScopeWindowSchema,
|
||||
@@ -55,8 +64,8 @@ const coachResultPeriodSchema = z.enum(["current", "previous", "yearAgo"]);
|
||||
// them are written back into the model's context (`m<k>.r<n>` lines, the
|
||||
// "keep looking" fetched list). Each is held to its exact server-minted
|
||||
// shape, so a stored row edited by hand cannot carry text into a prompt.
|
||||
/** A table of a message: `r1`..`r6`. */
|
||||
const resultRefSchema = z.string().regex(/^r[1-6]$/);
|
||||
/** A table of a message: `r1`..`r8`. */
|
||||
const resultRefSchema = z.string().regex(/^r[1-8]$/);
|
||||
/** A server-minted row id (cuid). */
|
||||
const messageIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
|
||||
|
||||
@@ -67,7 +76,7 @@ export const coachStepSchema = z
|
||||
id: z
|
||||
.string()
|
||||
.regex(/^s[1-9]\d{0,2}$/)
|
||||
.describe("`s1`..`s12`, unique within a turn."),
|
||||
.describe("`s1`..`s48`, unique within a turn."),
|
||||
tool: z.enum([...COACH_TOOL_NAMES, "show_result", "snapshot"]),
|
||||
labelKey: z
|
||||
.string()
|
||||
@@ -126,6 +135,18 @@ const coachChartSpecSchema = z.discriminatedUnion("kind", [
|
||||
x: z.string(),
|
||||
series: z.array(z.string()),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("compare"),
|
||||
mode: z.enum(["periods", "metrics"]),
|
||||
x: z.string(),
|
||||
a: z.string(),
|
||||
b: z.string(),
|
||||
axes: z.union([z.literal(1), z.literal(2)]),
|
||||
})
|
||||
.describe(
|
||||
"v1.41 — two series on one chart: two periods overlaid (`periods`, one axis) or two metrics (`metrics`, one or two axes). `a` and `b` name the value columns, `x` the shared column.",
|
||||
),
|
||||
z.object({
|
||||
kind: z.literal("bar"),
|
||||
x: z.string(),
|
||||
@@ -151,13 +172,13 @@ const coachResultSourceSchema = z.object({
|
||||
});
|
||||
|
||||
const coachResultMetaShape = {
|
||||
ref: resultRefSchema.describe("`r1`..`r6`, unique within a message."),
|
||||
ref: resultRefSchema.describe("`r1`..`r8`, unique within a message."),
|
||||
source: coachResultSourceSchema,
|
||||
shape: z.enum(["timeSeries", "categoryCounts", "distribution", "single"]),
|
||||
titleKey: z.string(),
|
||||
title: z.string(),
|
||||
rowCount: z.number().int().describe("The full row count, before any trim."),
|
||||
chartKind: z.enum(["line", "bar", "histogram"]).nullable(),
|
||||
chartKind: z.enum(["line", "compare", "bar", "histogram"]).nullable(),
|
||||
displayed: z
|
||||
.boolean()
|
||||
.describe(
|
||||
@@ -245,6 +266,50 @@ export const coachMethodSchema = z
|
||||
"How an answer was reached: which sources, windows, counts and aggregation. Never a health value.",
|
||||
});
|
||||
|
||||
// ── Choices and assumptions ───────────────────────────────────────────────
|
||||
|
||||
/** An id the server minted for a plan, a fact or a record event. */
|
||||
const choiceIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
|
||||
|
||||
const coachChoiceValueSchema = z
|
||||
.object({
|
||||
metric: coachScopeSourceSchema.optional(),
|
||||
window: coachWindowEnum.optional(),
|
||||
comparison: z
|
||||
.enum(["previous_period", "year_ago", "baseline_90d"])
|
||||
.optional()
|
||||
.describe("v1.41 — what a comparison is drawn against."),
|
||||
goal: choiceIdSchema
|
||||
.optional()
|
||||
.describe("v1.41 — an active plan or a goal fact, by id."),
|
||||
anchor: choiceIdSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — a medication start, an illness episode or a cycle phase, by id.",
|
||||
),
|
||||
})
|
||||
.describe("What a choice stands for: catalog tokens and ids, never text.");
|
||||
|
||||
const coachAssumptionKindSchema = z.enum(["metric", "window", "comparison"]);
|
||||
|
||||
const coachAssumptionOptionSchema = z.object({
|
||||
labelKey: z.string(),
|
||||
label: z.string(),
|
||||
value: coachChoiceValueSchema,
|
||||
});
|
||||
|
||||
export const coachAssumptionSchema = z
|
||||
.object({
|
||||
kind: coachAssumptionKindSchema,
|
||||
value: coachAssumptionOptionSchema,
|
||||
alternatives: z.array(coachAssumptionOptionSchema).describe("At most 3."),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachAssumption",
|
||||
description:
|
||||
"v1.41 — what an answer assumed instead of asking, from a catalog. Each alternative is offered as a `change_assumption` follow-up.",
|
||||
});
|
||||
|
||||
// ── Follow-ups ────────────────────────────────────────────────────────────
|
||||
|
||||
export const coachFollowUpSchema = z
|
||||
@@ -261,6 +326,7 @@ export const coachFollowUpSchema = z
|
||||
"as_table",
|
||||
"related_metric",
|
||||
"continue",
|
||||
"change_assumption",
|
||||
]),
|
||||
labelKey: z.string(),
|
||||
label: z
|
||||
@@ -269,7 +335,7 @@ export const coachFollowUpSchema = z
|
||||
anchor: z
|
||||
.object({
|
||||
// Empty for a related metric read without a table of its own.
|
||||
ref: z.string().regex(/^(?:r[1-6])?$/),
|
||||
ref: z.string().regex(/^(?:r[1-8])?$/),
|
||||
domain: coachStepDomainSchema,
|
||||
window: coachWindowEnum.optional(),
|
||||
granularity: coachResultGranularitySchema.optional(),
|
||||
@@ -282,6 +348,15 @@ export const coachFollowUpSchema = z
|
||||
"True when the chip is answered from a stored table without a model call.",
|
||||
),
|
||||
origin: z.enum(["server", "model"]),
|
||||
assumption: z
|
||||
.object({
|
||||
kind: coachAssumptionKindSchema,
|
||||
value: coachChoiceValueSchema,
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — on a `change_assumption` chip: the assumption it replaces and the alternative it picks.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachFollowUp",
|
||||
@@ -293,21 +368,27 @@ export const coachFollowUpSchema = z
|
||||
|
||||
export const coachClarificationSchema = z
|
||||
.object({
|
||||
kind: z.enum(["metric", "window", "context"]),
|
||||
kind: z
|
||||
.enum(["metric", "window", "comparison", "goal", "anchor", "context"])
|
||||
.describe("`comparison`, `goal` and `anchor` since v1.41."),
|
||||
choices: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().regex(/^c[1-4]$/),
|
||||
labelKey: z.string(),
|
||||
label: z.string(),
|
||||
value: z.object({
|
||||
metric: coachScopeSourceSchema.optional(),
|
||||
window: coachWindowEnum.optional(),
|
||||
}),
|
||||
value: coachChoiceValueSchema,
|
||||
}),
|
||||
)
|
||||
.describe("At most 4. Metric choices are always ones the record holds."),
|
||||
freeText: z.boolean().describe("Whether a typed answer is welcome."),
|
||||
assumption: z
|
||||
.string()
|
||||
.regex(/^c[1-4]$/)
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — the choice that applies when the person does not answer. The question names it, and it is the first choice.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachClarification",
|
||||
@@ -315,6 +396,178 @@ export const coachClarificationSchema = z
|
||||
"The choices for a clarifying question. The question itself is the assistant message's text. Answer with `clarification: { messageId, choiceId? }`.",
|
||||
});
|
||||
|
||||
// ── Activity, memory and plans (v1.41) ────────────────────────────────────
|
||||
|
||||
const coachStopReasonSchema = z.enum(["budget", "time", "cap", "no_progress"]);
|
||||
|
||||
export const coachStopSchema = z
|
||||
.object({
|
||||
reason: coachStopReasonSchema,
|
||||
rounds: z
|
||||
.number()
|
||||
.int()
|
||||
.describe("The rounds the turn ran, the final one included."),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachStop",
|
||||
description:
|
||||
"v1.41 — why the Coach answered before it was done reading: the token budget, the time budget, the round cap, or no new data.",
|
||||
});
|
||||
|
||||
const coachActivityMetaShape = {
|
||||
id: z
|
||||
.string()
|
||||
.regex(/^a[1-9]\d?$/)
|
||||
.describe("`a1`..`a99`, unique within a turn."),
|
||||
phase: z.enum([
|
||||
"thinking",
|
||||
"memory",
|
||||
"fetch",
|
||||
"digest",
|
||||
"checkpoint",
|
||||
"remember",
|
||||
"plan",
|
||||
"asking",
|
||||
"stop",
|
||||
"answer",
|
||||
]),
|
||||
status: z.enum(["running", "done", "empty", "failed"]),
|
||||
round: z.number().int().describe("The tool round, from 1."),
|
||||
labelKey: z
|
||||
.string()
|
||||
.describe("Closed catalog key (`insights.coach.activity.*`)."),
|
||||
label: z
|
||||
.string()
|
||||
.describe(
|
||||
"Rendered on the server in the request locale from the catalog; never model text.",
|
||||
),
|
||||
stepRef: z
|
||||
.string()
|
||||
.regex(/^s[1-9]\d{0,2}$/)
|
||||
.optional()
|
||||
.describe("The `step` a `fetch` entry stands for."),
|
||||
count: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe("Readings, lookups or entries the server counted."),
|
||||
durationMs: z.number().int().optional(),
|
||||
stop: coachStopReasonSchema.optional().describe("Set on the `stop` entry."),
|
||||
};
|
||||
|
||||
export const coachActivityMetaSchema = z.object(coachActivityMetaShape).meta({
|
||||
id: "CoachActivityMeta",
|
||||
description:
|
||||
"v1.41 — one entry of a turn's trail as persisted on `metricSource.activity`: phase, status, round, catalog label, counts. The model-written title and text are behind `GET /api/insights/chat/{id}/messages/{messageId}/trail`.",
|
||||
});
|
||||
|
||||
export const coachActivitySchema = z
|
||||
.object({
|
||||
...coachActivityMetaShape,
|
||||
title: z
|
||||
.string()
|
||||
.max(ACTIVITY_TITLE_MAX_CHARS)
|
||||
.optional()
|
||||
.describe("A screened reasoning title or checkpoint sentence."),
|
||||
text: z
|
||||
.string()
|
||||
.max(ACTIVITY_TEXT_MAX_CHARS)
|
||||
.optional()
|
||||
.describe("A screened reasoning summary for the round."),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachActivity",
|
||||
description:
|
||||
"v1.41 — one entry of the live trail, sent as `activity` frames that upsert by `id`. `title` and `text` are model text, screened, and sent only to the account that owns the conversation.",
|
||||
});
|
||||
|
||||
const coachFactCategorySchema = z.enum(COACH_MEMORY_CATEGORIES);
|
||||
|
||||
const coachMemoryNoteMetaShape = {
|
||||
proposal: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"`true`: waiting for the person, answered with `memoryDecision`. `false`: already saved; undo deletes `factId`.",
|
||||
),
|
||||
proposalId: choiceIdSchema.optional(),
|
||||
factId: choiceIdSchema.optional(),
|
||||
category: coachFactCategorySchema,
|
||||
};
|
||||
|
||||
export const coachMemoryNoteMetaSchema = z
|
||||
.object(coachMemoryNoteMetaShape)
|
||||
.meta({
|
||||
id: "CoachMemoryNoteMeta",
|
||||
description:
|
||||
"v1.41 — the fact a reply saved or proposes, as persisted on `metricSource.memoryNote`. No fact text.",
|
||||
});
|
||||
|
||||
export const coachMemoryNoteSchema = z
|
||||
.object({
|
||||
...coachMemoryNoteMetaShape,
|
||||
fact: z.string().max(REMEMBER_FACT_MAX_CHARS),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachMemoryNote",
|
||||
description:
|
||||
"v1.41 — a fact the Coach saved during the turn, or proposes to save (health facts are never saved without a tap). Owner only.",
|
||||
});
|
||||
|
||||
const coachPlanProposalMetaShape = {
|
||||
planId: choiceIdSchema,
|
||||
metric: z.string().max(64),
|
||||
reviewInDays: z
|
||||
.number()
|
||||
.int()
|
||||
.min(PLAN_REVIEW_DAYS.min)
|
||||
.max(PLAN_REVIEW_DAYS.max),
|
||||
};
|
||||
|
||||
export const coachPlanProposalMetaSchema = z
|
||||
.object(coachPlanProposalMetaShape)
|
||||
.meta({
|
||||
id: "CoachPlanProposalMeta",
|
||||
description:
|
||||
"v1.41 — the plan a reply proposes, as persisted on `metricSource.planProposal`. No plan text.",
|
||||
});
|
||||
|
||||
export const coachPlanProposalSchema = z
|
||||
.object({
|
||||
...coachPlanProposalMetaShape,
|
||||
ifCue: z.string(),
|
||||
thenAction: z.string(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachPlanProposal",
|
||||
description:
|
||||
"v1.41 — a plan the Coach proposes, written as `proposed`. Answer with `planDecision: { messageId, planId, accept }`. Owner only.",
|
||||
});
|
||||
|
||||
export const coachTrailSchema = z
|
||||
.object({
|
||||
entries: z.array(
|
||||
z.object({
|
||||
id: z.string().regex(/^a[1-9]\d?$/),
|
||||
title: z.string().max(ACTIVITY_TITLE_MAX_CHARS).optional(),
|
||||
text: z.string().max(ACTIVITY_TEXT_MAX_CHARS).optional(),
|
||||
}),
|
||||
),
|
||||
recalled: z.array(z.string().max(REMEMBER_FACT_MAX_CHARS)).optional(),
|
||||
proposal: z
|
||||
.object({
|
||||
proposalId: choiceIdSchema,
|
||||
category: coachFactCategorySchema,
|
||||
fact: z.string().max(REMEMBER_FACT_MAX_CHARS),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachTrail",
|
||||
description:
|
||||
"v1.41 — the model-written text of a turn's trail and the fact texts it touched. Encrypted at rest and served only to the account that owns the conversation.",
|
||||
});
|
||||
|
||||
// ── The cards and the usage envelope ──────────────────────────────────────
|
||||
|
||||
const coachSuggestionSchema = z
|
||||
@@ -457,6 +710,27 @@ export const coachProvenanceSchema = z
|
||||
.describe(
|
||||
"v1.39.4 — this reply continues an answer that was forced at the round cap (the `continue` chip): the id of that earlier assistant message. A continuation offers no further `continue` chip. Absent otherwise.",
|
||||
),
|
||||
activity: z
|
||||
.array(coachActivityMetaSchema)
|
||||
.optional()
|
||||
.describe("v1.41 — the turn's trail, metadata only."),
|
||||
stop: coachStopSchema
|
||||
.optional()
|
||||
.describe("v1.41 — why the answer was forced, when it was."),
|
||||
assumptions: z
|
||||
.array(coachAssumptionSchema)
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — what the answer assumed instead of asking, at most two.",
|
||||
),
|
||||
memoryNote: coachMemoryNoteMetaSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — the fact this reply saved or proposes, without its text.",
|
||||
),
|
||||
planProposal: coachPlanProposalMetaSchema
|
||||
.optional()
|
||||
.describe("v1.41 — the plan this reply proposes, without its text."),
|
||||
})
|
||||
.meta({
|
||||
id: "CoachProvenance",
|
||||
@@ -487,6 +761,13 @@ export const coachStreamEventSchema = z
|
||||
conversationId: z.string(),
|
||||
messageId: z.string(),
|
||||
usage: coachUsageSchema.optional(),
|
||||
stop: coachStopSchema.optional(),
|
||||
withheldResults: z
|
||||
.literal(true)
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — the turn was blocked after interim tables went out; remove them.",
|
||||
),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("error"),
|
||||
@@ -495,7 +776,14 @@ export const coachStreamEventSchema = z
|
||||
reason: z.enum(AI_UNAVAILABLE_REASONS).optional(),
|
||||
}),
|
||||
z.object({ type: z.literal("step"), step: coachStepSchema }),
|
||||
z.object({ type: z.literal("result"), result: coachResultTableSchema }),
|
||||
z.object({
|
||||
type: z.literal("result"),
|
||||
result: coachResultTableSchema,
|
||||
interim: z
|
||||
.literal(true)
|
||||
.optional()
|
||||
.describe("v1.41 — sent while the turn still runs."),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("followUps"),
|
||||
followUps: z.array(coachFollowUpSchema),
|
||||
@@ -504,9 +792,15 @@ export const coachStreamEventSchema = z
|
||||
type: z.literal("clarification"),
|
||||
clarification: coachClarificationSchema,
|
||||
}),
|
||||
z.object({ type: z.literal("activity"), activity: coachActivitySchema }),
|
||||
z.object({ type: z.literal("memoryNote"), note: coachMemoryNoteSchema }),
|
||||
z.object({
|
||||
type: z.literal("planProposal"),
|
||||
proposal: coachPlanProposalSchema,
|
||||
}),
|
||||
])
|
||||
.meta({
|
||||
id: "CoachStreamEvent",
|
||||
description:
|
||||
"One Server-Sent Events frame of a Coach turn, sent as `data: <json>\\n\\n` and dispatched on `type`. Order: `step*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` → `clarification?` → `followUps?` → `done`, or a single `error`. Clients ignore a `type` they do not know.",
|
||||
"One Server-Sent Events frame of a Coach turn, sent as `data: <json>\\n\\n` and dispatched on `type`. Order: `(activity | step)*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` → `memoryNote?` → `planProposal?` → `clarification?` → `followUps?` → `done`, or a single `error`. `result` frames with `interim: true` may also arrive among the `activity` frames. Clients ignore a `type` they do not know.",
|
||||
});
|
||||
+274
-7
@@ -183,6 +183,32 @@ export const coachChatRequestSchema = z.object({
|
||||
choiceId: z.string().min(1).max(8).optional(),
|
||||
})
|
||||
.optional(),
|
||||
/**
|
||||
* v1.41 — the person answered a fact proposal ("Yes, remember it" / "No").
|
||||
* `messageId` is the assistant message that offered it, `proposalId` the
|
||||
* proposal on that message. The client never sends the fact: the server
|
||||
* reads it from what it stored on that message, so a tap can only accept
|
||||
* what the Coach actually proposed.
|
||||
*/
|
||||
memoryDecision: z
|
||||
.object({
|
||||
messageId: z.string().min(1).max(64),
|
||||
proposalId: z.string().min(1).max(64),
|
||||
accept: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
/**
|
||||
* v1.41 — the person answered a plan proposal ("Take on this plan" /
|
||||
* "Not now"). `planId` is the proposed plan the assistant message carried;
|
||||
* the server holds it to that message and to the person's own plans.
|
||||
*/
|
||||
planDecision: z
|
||||
.object({
|
||||
messageId: z.string().min(1).max(64),
|
||||
planId: z.string().min(1).max(64),
|
||||
accept: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -247,6 +273,18 @@ export interface CoachUsage {
|
||||
* step* → token* → provenance → result* → suggestion? →
|
||||
* suggestedAction? → clarification? → followUps? → done
|
||||
*
|
||||
* v1.41 — three more additive frames: `activity` (the live trail: one
|
||||
* entry per phase of the turn, upserted by id), `memoryNote` (a fact the
|
||||
* Coach saved or proposes to save) and `planProposal` (a plan it proposes).
|
||||
* `result` may carry `interim: true` while the turn still runs, and `done`
|
||||
* carries `stop` when the answer was forced and `withheldResults` when the
|
||||
* interim tables must be taken down again. The `reasoning` frame stays
|
||||
* accepted and is never sent. Frame order:
|
||||
*
|
||||
* (activity | step)* → token* → provenance → result* → suggestion? →
|
||||
* suggestedAction? → memoryNote? → planProposal? → clarification? →
|
||||
* followUps? → done
|
||||
*
|
||||
* No new frame carries a top-level key the older native client decodes
|
||||
* (`token`, `conversationId`, `messageId`, `code`, `message`, `suggestion`,
|
||||
* `metricSource`, `usage`), so an older client drops them whole.
|
||||
@@ -268,6 +306,13 @@ export type CoachStreamEvent =
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
usage?: CoachUsage;
|
||||
/** v1.41 — why the answer was forced, when it was. */
|
||||
stop?: CoachStop;
|
||||
/**
|
||||
* v1.41 — the turn was blocked after interim tables went out; the
|
||||
* client removes them.
|
||||
*/
|
||||
withheldResults?: true;
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
@@ -281,9 +326,155 @@ export type CoachStreamEvent =
|
||||
reason?: AiUnavailableReason;
|
||||
}
|
||||
| { type: "step"; step: CoachStep }
|
||||
| { type: "result"; result: CoachResultTable }
|
||||
| {
|
||||
type: "result";
|
||||
result: CoachResultTable;
|
||||
/**
|
||||
* v1.41 — sent while the turn still runs, right after the call that
|
||||
* read it. The answer decides later whether it is shown or sits under
|
||||
* "Data used".
|
||||
*/
|
||||
interim?: true;
|
||||
}
|
||||
| { type: "followUps"; followUps: CoachFollowUp[] }
|
||||
| { type: "clarification"; clarification: CoachClarification };
|
||||
| { type: "clarification"; clarification: CoachClarification }
|
||||
| { type: "activity"; activity: CoachActivity }
|
||||
| { type: "memoryNote"; note: CoachMemoryNote }
|
||||
| { type: "planProposal"; proposal: CoachPlanProposal };
|
||||
|
||||
// ── Activity (v1.41) ────────────────────────────────────────────────────
|
||||
// The live trail of a turn: what the Coach is doing right now, one entry per
|
||||
// phase. The metadata is plaintext and persisted on `metricSource.activity`;
|
||||
// the model-written `title` and `text` reach only the owner and are stored
|
||||
// encrypted in `coach_messages.trail_encrypted`.
|
||||
|
||||
export type CoachActivityPhase =
|
||||
| "thinking"
|
||||
| "memory"
|
||||
| "fetch"
|
||||
| "digest"
|
||||
| "checkpoint"
|
||||
| "remember"
|
||||
| "plan"
|
||||
| "asking"
|
||||
| "stop"
|
||||
| "answer";
|
||||
|
||||
/** Why the loop forced its final answer. */
|
||||
export type CoachStopReason = "budget" | "time" | "cap" | "no_progress";
|
||||
|
||||
export interface CoachStop {
|
||||
reason: CoachStopReason;
|
||||
/** The rounds the turn ran, the final one included. */
|
||||
rounds: number;
|
||||
}
|
||||
|
||||
/** Plaintext; rides `metricSource.activity`. */
|
||||
export interface CoachActivityMeta {
|
||||
/** `a1`..`a99`, unique within a turn; frames upsert by id. */
|
||||
id: string;
|
||||
phase: CoachActivityPhase;
|
||||
status: CoachStepStatus;
|
||||
/** The tool round the entry belongs to, from 1. */
|
||||
round: number;
|
||||
/** Closed catalog key (`insights.coach.activity.*`). */
|
||||
labelKey: string;
|
||||
/**
|
||||
* The label rendered on the server in the request locale. Always catalog
|
||||
* text with server-chosen values, never model text.
|
||||
*/
|
||||
label: string;
|
||||
/** The `step` a `fetch` entry stands for (`s1`..). */
|
||||
stepRef?: string;
|
||||
/** Readings, lookups or remembered entries the server counted. */
|
||||
count?: number;
|
||||
durationMs?: number;
|
||||
/** Set on the `stop` entry. */
|
||||
stop?: CoachStopReason;
|
||||
}
|
||||
|
||||
/** One trail entry on the wire: the metadata plus the owner-only model text. */
|
||||
export interface CoachActivity extends CoachActivityMeta {
|
||||
/** A screened reasoning title or checkpoint sentence, at most 80 chars. */
|
||||
title?: string;
|
||||
/** A screened reasoning summary for the round, at most 400 chars. */
|
||||
text?: string;
|
||||
}
|
||||
|
||||
// ── Memory and plans (v1.41) ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The categories a Coach fact is filed under. The extraction's list in
|
||||
* `facts.ts` plus `medication`, which only the in-turn `remember_fact` tool
|
||||
* and the remember button write. `condition`, `constraint` and `medication`
|
||||
* are health facts: never saved without the person's tap.
|
||||
*/
|
||||
export const COACH_MEMORY_CATEGORIES = [
|
||||
"preference",
|
||||
"goal",
|
||||
"context",
|
||||
"condition",
|
||||
"constraint",
|
||||
"medication",
|
||||
] as const;
|
||||
|
||||
export type CoachMemoryCategory = (typeof COACH_MEMORY_CATEGORIES)[number];
|
||||
|
||||
/** Plaintext; rides `metricSource.memoryNote`. No fact text. */
|
||||
export interface CoachMemoryNoteMeta {
|
||||
/**
|
||||
* `true`: a proposal waiting for the person ("Yes, remember it" / "No"),
|
||||
* answered with `memoryDecision`. `false`: already saved, undone by
|
||||
* deleting `factId`.
|
||||
*/
|
||||
proposal: boolean;
|
||||
/** The proposal on this message (`proposal: true`). */
|
||||
proposalId?: string;
|
||||
/** The saved fact (`proposal: false`). */
|
||||
factId?: string;
|
||||
category: CoachMemoryCategory;
|
||||
}
|
||||
|
||||
/** On the wire, owner only: the metadata plus the fact itself. */
|
||||
export interface CoachMemoryNote extends CoachMemoryNoteMeta {
|
||||
/** The fact as it will be stored, at most 160 chars. */
|
||||
fact: string;
|
||||
}
|
||||
|
||||
/** Plaintext; rides `metricSource.planProposal`. No plan text. */
|
||||
export interface CoachPlanProposalMeta {
|
||||
/** The `CoachPlan` row, written as `proposed`. */
|
||||
planId: string;
|
||||
/** The metric the plan moves, e.g. `WEIGHT`. */
|
||||
metric: string;
|
||||
/** 7..56. */
|
||||
reviewInDays: number;
|
||||
}
|
||||
|
||||
/** On the wire, owner only: the metadata plus the plan's own words. */
|
||||
export interface CoachPlanProposal extends CoachPlanProposalMeta {
|
||||
ifCue: string;
|
||||
thenAction: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.41 — the decrypted `coach_messages.trail_encrypted` of one message, as
|
||||
* `GET …/messages/{messageId}/trail` serves it to the owner. Model text and
|
||||
* fact text only; the structure lives in `metricSource`.
|
||||
*/
|
||||
export interface CoachTrail {
|
||||
/** Title and text per activity entry, by `CoachActivityMeta.id`. */
|
||||
entries: Array<{ id: string; title?: string; text?: string }>;
|
||||
/** The facts recalled into the turn (the `memory` entry). */
|
||||
recalled?: string[];
|
||||
/** The fact proposal's text, which a `memoryDecision` reads back. */
|
||||
proposal?: {
|
||||
proposalId: string;
|
||||
category: CoachMemoryCategory;
|
||||
fact: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Steps (v1.39.4) ─────────────────────────────────────────────────────
|
||||
// What the Coach is reading while a turn runs. Labels come from a closed
|
||||
@@ -307,7 +498,7 @@ export type CoachStepReason =
|
||||
| "invalid_arguments";
|
||||
|
||||
export interface CoachStep {
|
||||
/** `s1`..`s12`, unique within a turn; frames upsert by id. */
|
||||
/** `s1`..`s48`, unique within a turn; frames upsert by id. */
|
||||
id: string;
|
||||
tool: CoachToolName | "show_result" | "snapshot";
|
||||
/** Closed catalog key, e.g. `coach.step.read`. */
|
||||
@@ -351,6 +542,19 @@ export type CoachResultCell = string | number | null;
|
||||
|
||||
export type CoachChartSpec =
|
||||
| { kind: "line"; x: string; series: string[] }
|
||||
| {
|
||||
/**
|
||||
* v1.41 — two series on one chart: two periods overlaid (`periods`,
|
||||
* one axis) or two metrics side by side (`metrics`, one or two axes).
|
||||
* `a` and `b` name the value columns, `x` the shared column.
|
||||
*/
|
||||
kind: "compare";
|
||||
mode: "periods" | "metrics";
|
||||
x: string;
|
||||
a: string;
|
||||
b: string;
|
||||
axes: 1 | 2;
|
||||
}
|
||||
| {
|
||||
kind: "bar";
|
||||
x: string;
|
||||
@@ -374,7 +578,7 @@ export interface CoachResultSource {
|
||||
|
||||
/** Plaintext; rides `metricSource.results`. */
|
||||
export interface CoachResultMeta {
|
||||
/** `r1`..`r6`, per message. */
|
||||
/** `r1`..`r8`, per message. */
|
||||
ref: string;
|
||||
source: CoachResultSource;
|
||||
shape: CoachResultShape;
|
||||
@@ -445,7 +649,8 @@ export type CoachFollowUpKind =
|
||||
| "as_chart"
|
||||
| "as_table"
|
||||
| "related_metric"
|
||||
| "continue";
|
||||
| "continue"
|
||||
| "change_assumption";
|
||||
|
||||
export interface CoachFollowUp {
|
||||
/** `f1`..`f3`. */
|
||||
@@ -463,25 +668,77 @@ export interface CoachFollowUp {
|
||||
/** True when the chip is answered from a stored table, without a model. */
|
||||
reuse: boolean;
|
||||
origin: "server" | "model";
|
||||
/**
|
||||
* v1.41 — on a `change_assumption` chip: the assumption it replaces and
|
||||
* the alternative it picks.
|
||||
*/
|
||||
assumption?: { kind: CoachAssumptionKind; value: CoachChoiceValue };
|
||||
}
|
||||
|
||||
// ── Clarification (v1.39.4) ─────────────────────────────────────────────
|
||||
// A question with choices. The question text is the assistant message
|
||||
// itself, encrypted like any reply; only the choices ride here.
|
||||
|
||||
/** v1.41 — what a comparison is drawn against. */
|
||||
export type CoachComparisonBasis =
|
||||
"previous_period" | "year_ago" | "baseline_90d";
|
||||
|
||||
/**
|
||||
* What a choice stands for. One field per kind; `goal` and `anchor` are ids
|
||||
* of the person's own plans, facts or record events, never text.
|
||||
*/
|
||||
export interface CoachChoiceValue {
|
||||
metric?: CoachScopeSource;
|
||||
window?: CoachScopeWindow;
|
||||
/** v1.41 */
|
||||
comparison?: CoachComparisonBasis;
|
||||
/** v1.41 — an active plan or a goal fact. */
|
||||
goal?: string;
|
||||
/** v1.41 — a medication start, an illness episode, a cycle phase. */
|
||||
anchor?: string;
|
||||
}
|
||||
|
||||
export interface CoachClarificationChoice {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
label: string;
|
||||
value: { metric?: CoachScopeSource; window?: CoachScopeWindow };
|
||||
value: CoachChoiceValue;
|
||||
}
|
||||
|
||||
export type CoachClarificationKind =
|
||||
"metric" | "window" | "comparison" | "goal" | "anchor" | "context";
|
||||
|
||||
export interface CoachClarification {
|
||||
kind: "metric" | "window" | "context";
|
||||
/** `comparison`, `goal` and `anchor` since v1.41. */
|
||||
kind: CoachClarificationKind;
|
||||
/** At most 4; metric choices are always ones the record holds. */
|
||||
choices: CoachClarificationChoice[];
|
||||
/** Whether a typed answer is welcome beside the choices. */
|
||||
freeText: boolean;
|
||||
/**
|
||||
* v1.41 — the choice that applies when the person does not answer (`c1`..);
|
||||
* the question names it, and it is the first choice.
|
||||
*/
|
||||
assumption?: string;
|
||||
}
|
||||
|
||||
// ── Assumptions (v1.41) ─────────────────────────────────────────────────
|
||||
// What an answer assumed instead of asking. Catalog values only, at most two
|
||||
// per answer; each offers its alternatives as `change_assumption` chips.
|
||||
|
||||
export type CoachAssumptionKind = "metric" | "window" | "comparison";
|
||||
|
||||
export interface CoachAssumptionOption {
|
||||
labelKey: string;
|
||||
label: string;
|
||||
value: CoachChoiceValue;
|
||||
}
|
||||
|
||||
export interface CoachAssumption {
|
||||
kind: CoachAssumptionKind;
|
||||
value: CoachAssumptionOption;
|
||||
/** At most 3. */
|
||||
alternatives: CoachAssumptionOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -672,6 +929,16 @@ export interface CoachProvenance {
|
||||
* further "keep looking" chip, so an answer is continued at most once.
|
||||
*/
|
||||
continuationOf?: string;
|
||||
/** v1.41 — the turn's trail, metadata only; the text is behind `…/trail`. */
|
||||
activity?: CoachActivityMeta[];
|
||||
/** v1.41 — why the answer was forced, when it was. */
|
||||
stop?: CoachStop;
|
||||
/** v1.41 — what the answer assumed instead of asking, at most two. */
|
||||
assumptions?: CoachAssumption[];
|
||||
/** v1.41 — the fact this reply saved or proposes, without its text. */
|
||||
memoryNote?: CoachMemoryNoteMeta;
|
||||
/** v1.41 — the plan this reply proposes, without its text. */
|
||||
planProposal?: CoachPlanProposalMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
BACKGROUND_REASONING_JOBS,
|
||||
BACKGROUND_REASONING_LEVEL,
|
||||
DEFAULT_REASONING_LEVEL,
|
||||
DEFAULT_REASONING_MAX_EFFORT,
|
||||
REASONING_LEVELS,
|
||||
REASONING_MAX_EFFORTS,
|
||||
REASONING_THINKING_BUDGET,
|
||||
capReasoningLevel,
|
||||
isReasoningLevel,
|
||||
} from "../levels";
|
||||
|
||||
describe("reasoning levels", () => {
|
||||
it("defaults to medium for a person and to no cap for an instance", () => {
|
||||
expect(DEFAULT_REASONING_LEVEL).toBe("medium");
|
||||
expect(DEFAULT_REASONING_MAX_EFFORT).toBe("high");
|
||||
expect(REASONING_MAX_EFFORTS).toEqual(
|
||||
REASONING_LEVELS.filter((level) => level !== "off"),
|
||||
);
|
||||
});
|
||||
|
||||
it("recognises exactly the four levels", () => {
|
||||
for (const level of REASONING_LEVELS) {
|
||||
expect(isReasoningLevel(level)).toBe(true);
|
||||
}
|
||||
for (const other of ["none", "minimal", "xhigh", "", null, 2]) {
|
||||
expect(isReasoningLevel(other)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("caps a level at the operator's maximum and never raises one", () => {
|
||||
expect(capReasoningLevel("high", "medium")).toBe("medium");
|
||||
expect(capReasoningLevel("high", "low")).toBe("low");
|
||||
expect(capReasoningLevel("medium", "high")).toBe("medium");
|
||||
expect(capReasoningLevel("low", "medium")).toBe("low");
|
||||
expect(capReasoningLevel("off", "low")).toBe("off");
|
||||
});
|
||||
|
||||
it("gives every level above off a growing thinking budget of at least 1024", () => {
|
||||
const budgets = REASONING_LEVELS.filter((level) => level !== "off").map(
|
||||
(level) => REASONING_THINKING_BUDGET[level as "low"],
|
||||
);
|
||||
expect(budgets[0]).toBeGreaterThanOrEqual(1_024);
|
||||
expect([...budgets].sort((a, b) => a - b)).toEqual(budgets);
|
||||
});
|
||||
|
||||
it("has a level for every background job, operator-paid never above its own", () => {
|
||||
expect(Object.keys(BACKGROUND_REASONING_LEVEL).sort()).toEqual(
|
||||
[...BACKGROUND_REASONING_JOBS].sort(),
|
||||
);
|
||||
for (const job of BACKGROUND_REASONING_JOBS) {
|
||||
const { level, operatorLevel } = BACKGROUND_REASONING_LEVEL[job];
|
||||
expect(level).not.toBe("off");
|
||||
expect(REASONING_LEVELS.indexOf(operatorLevel)).toBeLessThanOrEqual(
|
||||
REASONING_LEVELS.indexOf(level),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* v1.41 — the reasoning levels, the thinking budget behind each, and the
|
||||
* background jobs that may reason.
|
||||
*
|
||||
* One vocabulary for every layer: the person's Coach preference
|
||||
* (`coachPrefsJson.reasoning`), the operator's cap
|
||||
* (`app_settings.ai_reasoning_max_effort`), the resolved value a call carries
|
||||
* (`CompletionParams.reasoning.effort`) and the level `/api/auth/me` publishes.
|
||||
* Each provider client maps it onto its own wire; nothing outside a client
|
||||
* speaks a provider's dialect.
|
||||
*
|
||||
* A leaf on purpose, like `../reasoning-effort.ts`: `types.ts` imports it, and
|
||||
* `types.ts` is reachable from workers that must not reach the provider
|
||||
* machinery. No import from `provider-chain.ts`, and none from a client.
|
||||
*
|
||||
* Client-safe: no server import.
|
||||
*/
|
||||
|
||||
/**
|
||||
* `off` is the person's choice, not a promise about the wire: where a provider
|
||||
* cannot switch reasoning off, the client sends its lowest setting instead and
|
||||
* the settings say "Minimal" rather than "Off" (`offIsReal` in
|
||||
* `support.ts`).
|
||||
*/
|
||||
export const REASONING_LEVELS = ["off", "low", "medium", "high"] as const;
|
||||
|
||||
export type ReasoningLevel = (typeof REASONING_LEVELS)[number];
|
||||
|
||||
/** The level an account that never chose one gets. */
|
||||
export const DEFAULT_REASONING_LEVEL: ReasoningLevel = "medium";
|
||||
|
||||
/** The values the operator's cap may take; switching off is the other field. */
|
||||
export const REASONING_MAX_EFFORTS = ["low", "medium", "high"] as const;
|
||||
|
||||
export type ReasoningMaxEffort = (typeof REASONING_MAX_EFFORTS)[number];
|
||||
|
||||
/** The cap an instance that never set one has: no cap. */
|
||||
export const DEFAULT_REASONING_MAX_EFFORT: ReasoningMaxEffort = "high";
|
||||
|
||||
export function isReasoningLevel(value: unknown): value is ReasoningLevel {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
(REASONING_LEVELS as readonly string[]).includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
/** `level`, lowered to `max` when it is above it. */
|
||||
export function capReasoningLevel(
|
||||
level: ReasoningLevel,
|
||||
max: ReasoningMaxEffort,
|
||||
): ReasoningLevel {
|
||||
return REASONING_LEVELS.indexOf(level) > REASONING_LEVELS.indexOf(max)
|
||||
? max
|
||||
: level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thinking tokens per level, for the providers that take a token budget
|
||||
* rather than a named effort (Anthropic's manual mode, gateways that accept
|
||||
* `max_tokens`). A round's output allowance is its answer budget plus this.
|
||||
*/
|
||||
export const REASONING_THINKING_BUDGET: Readonly<
|
||||
Record<Exclude<ReasoningLevel, "off">, number>
|
||||
> = {
|
||||
low: 1_024,
|
||||
medium: 4_096,
|
||||
high: 12_000,
|
||||
};
|
||||
|
||||
/**
|
||||
* The background jobs that may reason. Every other unattended surface (status
|
||||
* cards, batch notes, archetypes, workout and reaction lines, nudges, document
|
||||
* reading, OCR) never does, and the inventory test freezes this list.
|
||||
*/
|
||||
export const BACKGROUND_REASONING_JOBS = [
|
||||
"daily_briefing",
|
||||
"period_narrative_month",
|
||||
"period_narrative_week",
|
||||
"memory_summary",
|
||||
"memory_facts",
|
||||
"memory_plans",
|
||||
] as const;
|
||||
|
||||
export type BackgroundReasoningJob = (typeof BACKGROUND_REASONING_JOBS)[number];
|
||||
|
||||
/**
|
||||
* The level each job asks for: `level` on the person's own plan, key or local
|
||||
* model, `operatorLevel` when the operator pays for the call. Still capped by
|
||||
* the operator's settings like every other call.
|
||||
*/
|
||||
export const BACKGROUND_REASONING_LEVEL: Readonly<
|
||||
Record<
|
||||
BackgroundReasoningJob,
|
||||
{ level: ReasoningLevel; operatorLevel: ReasoningLevel }
|
||||
>
|
||||
> = {
|
||||
daily_briefing: { level: "medium", operatorLevel: "low" },
|
||||
period_narrative_month: { level: "medium", operatorLevel: "low" },
|
||||
period_narrative_week: { level: "low", operatorLevel: "low" },
|
||||
memory_summary: { level: "low", operatorLevel: "low" },
|
||||
memory_facts: { level: "low", operatorLevel: "low" },
|
||||
memory_plans: { level: "medium", operatorLevel: "low" },
|
||||
};
|
||||
|
||||
// ── Message keys ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* One option label per level. `minimal` stands in for `off` where the active
|
||||
* provider cannot switch reasoning off.
|
||||
*/
|
||||
export const REASONING_LEVEL_LABEL_KEYS = {
|
||||
off: "insights.coach.reasoning.off",
|
||||
minimal: "insights.coach.reasoning.minimal",
|
||||
low: "insights.coach.reasoning.low",
|
||||
medium: "insights.coach.reasoning.medium",
|
||||
high: "insights.coach.reasoning.high",
|
||||
} as const;
|
||||
|
||||
/** The Coach settings field around the options. */
|
||||
export const REASONING_SETTING_KEYS = {
|
||||
label: "insights.coach.reasoning.label",
|
||||
hint: "insights.coach.reasoning.hint",
|
||||
capped: "insights.coach.reasoning.capped",
|
||||
disabled: "insights.coach.reasoning.disabled",
|
||||
} as const;
|
||||
|
||||
/** The operator's two controls in the admin assistant section. */
|
||||
export const REASONING_ADMIN_KEYS = {
|
||||
enabled: "admin.assistant.reasoning.enabled",
|
||||
max: "admin.assistant.reasoning.max",
|
||||
} as const;
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReasoningEffort } from "./reasoning-effort";
|
||||
import type { ReasoningLevel } from "./reasoning/levels";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// ─── Insight Result Schema ─────────────────────────────────
|
||||
@@ -252,6 +253,37 @@ export interface AiMessage {
|
||||
* A no-op on every other provider (their prefix caching is automatic).
|
||||
*/
|
||||
cacheBreakpoint?: boolean;
|
||||
/**
|
||||
* v1.41 — opaque provider state carried from one tool round to the next on
|
||||
* an assistant turn: the reasoning items a provider requires back verbatim
|
||||
* (encrypted reasoning items, thinking blocks with their signatures,
|
||||
* reasoning details). Only the client whose `providerType` and `model` match
|
||||
* re-inserts it; every other client drops it. Lives in memory for one turn:
|
||||
* never persisted, never logged, never passed to `annotate()`.
|
||||
*/
|
||||
providerState?: AiProviderState;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.41 — provider state threaded between the rounds of one turn. `items` is
|
||||
* whatever the producing client needs back, unchanged; nothing outside that
|
||||
* client reads it.
|
||||
*/
|
||||
export interface AiProviderState {
|
||||
providerType: ProviderType;
|
||||
model: string;
|
||||
items: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.41 — one live reasoning event a client reports while a call runs.
|
||||
* `title` is the first bold line of a summary once it has closed, `text` a
|
||||
* finished summary, `done` the end of the reasoning for this call. Model
|
||||
* text: the receiver screens it before anything reaches a person.
|
||||
*/
|
||||
export interface AiReasoningEvent {
|
||||
kind: "title" | "text" | "done";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface CompletionParams {
|
||||
@@ -334,6 +366,20 @@ export interface CompletionParams {
|
||||
* `ai-call-timeout-guard.test.ts`.
|
||||
*/
|
||||
timeoutPolicy?: "surface-ceiling";
|
||||
/**
|
||||
* v1.41 — the reasoning this call asks for, already resolved against the
|
||||
* person's level, the operator's cap and what the provider supports
|
||||
* (`src/lib/ai/reasoning/resolve.ts`). `summaries` asks for reasoning
|
||||
* summaries where the provider offers them. Absent → today's behaviour: the
|
||||
* client sends exactly what it sent before the field existed.
|
||||
*/
|
||||
reasoning?: { effort: ReasoningLevel; summaries: boolean };
|
||||
/**
|
||||
* v1.41 — live reasoning events, called as they arrive on clients that read
|
||||
* the provider's stream, or once after the reply on clients that do not.
|
||||
* Never called when `reasoning` is absent.
|
||||
*/
|
||||
onReasoning?: (event: AiReasoningEvent) => void;
|
||||
}
|
||||
|
||||
export interface CompletionResult {
|
||||
@@ -358,6 +404,22 @@ export interface CompletionResult {
|
||||
* `"tool_calls"`. Absent when the provider does not surface it.
|
||||
*/
|
||||
finishReason?: "stop" | "tool_calls" | "length";
|
||||
/**
|
||||
* v1.41 — what the call reasoned, when it reasoned. `summary` holds the
|
||||
* finished summaries (model text, unscreened), `tokens` the reasoning tokens
|
||||
* the provider billed where it reports them, `downgradedTo` the level the
|
||||
* client fell back to after the provider refused the requested one.
|
||||
*/
|
||||
reasoning?: {
|
||||
summary: string[];
|
||||
tokens: number | null;
|
||||
downgradedTo?: ReasoningLevel;
|
||||
};
|
||||
/**
|
||||
* v1.41 — the state the next round of the same turn hands back on its
|
||||
* assistant message (see {@link AiMessage.providerState}).
|
||||
*/
|
||||
providerState?: AiProviderState;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -232,6 +232,8 @@ export const ENCRYPTED_COLUMNS: readonly EncryptedColumn[] = [
|
||||
{ model: "CoachMessage", field: "encryptedContent", kind: "bytes" },
|
||||
// v1.39.4 — the tables of values a turn read, JSON under the same codec.
|
||||
{ model: "CoachMessage", field: "resultsEncrypted", kind: "bytes" },
|
||||
// v1.41 — the model text of the turn's trail, JSON under the same codec.
|
||||
{ model: "CoachMessage", field: "trailEncrypted", kind: "bytes" },
|
||||
{ model: "CoachConversation", field: "summaryEncrypted", kind: "bytes" },
|
||||
// v1.39.3 — the title: the opening words of the first message, or a rename.
|
||||
{ model: "CoachConversation", field: "titleEncrypted", kind: "bytes" },
|
||||
|
||||
@@ -90,6 +90,16 @@ export interface CoachMessageBackupEntry {
|
||||
* unreadable-row marker stands in for tables that did not decrypt.
|
||||
*/
|
||||
resultsJson?: string | null;
|
||||
/**
|
||||
* v1.41 — the model text of the turn's trail, as ciphertext in base64.
|
||||
* Present on a disaster-recovery payload only; null on a turn without one.
|
||||
*/
|
||||
trailEncrypted?: string | null;
|
||||
/**
|
||||
* v1.41 — the same trail as readable JSON (a `CoachTrail`), on a portable
|
||||
* payload only; null on a turn without one.
|
||||
*/
|
||||
trailJson?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -167,6 +177,7 @@ const COACH_CONVERSATION_BACKUP_SELECT = {
|
||||
tokensUsed: true,
|
||||
model: true,
|
||||
resultsEncrypted: true,
|
||||
trailEncrypted: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
@@ -268,6 +279,14 @@ export async function buildCoachBackupSection(
|
||||
: null,
|
||||
}
|
||||
: { resultsJson: decryptTurnSoft(message.resultsEncrypted) }),
|
||||
// v1.41 — and so does the trail.
|
||||
...(disasterRecovery
|
||||
? {
|
||||
trailEncrypted: message.trailEncrypted
|
||||
? Buffer.from(message.trailEncrypted).toString("base64")
|
||||
: null,
|
||||
}
|
||||
: { trailJson: decryptTurnSoft(message.trailEncrypted) }),
|
||||
createdAt: message.createdAt.toISOString(),
|
||||
})),
|
||||
attachments: row.attachments.map((attachment) => ({
|
||||
@@ -340,6 +359,8 @@ export type RestoredCoachMessage = Pick<
|
||||
| "model"
|
||||
| "resultsEncrypted"
|
||||
| "resultsJson"
|
||||
| "trailEncrypted"
|
||||
| "trailJson"
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -424,6 +445,7 @@ export async function restoreCoachData(
|
||||
tokensUsed: message.tokensUsed ?? null,
|
||||
model: message.model ?? null,
|
||||
resultsEncrypted: resolveResultsBytes(message),
|
||||
trailEncrypted: resolveTrailBytes(message),
|
||||
createdAt: new Date(message.createdAt),
|
||||
})),
|
||||
},
|
||||
@@ -497,6 +519,18 @@ function resolveResultsBytes(
|
||||
return message.resultsJson ? encryptToBytes(message.resultsJson) : null;
|
||||
}
|
||||
|
||||
/** v1.41 — a turn's stored trail, from either end of the contract. */
|
||||
function resolveTrailBytes(
|
||||
message: RestoredCoachMessage,
|
||||
): Uint8Array<ArrayBuffer> | null {
|
||||
if (message.trailEncrypted !== undefined) {
|
||||
return message.trailEncrypted === null
|
||||
? null
|
||||
: decodeBase64(message.trailEncrypted);
|
||||
}
|
||||
return message.trailJson ? encryptToBytes(message.trailJson) : null;
|
||||
}
|
||||
|
||||
function resolveTitleBytes(
|
||||
conversation: RestoredCoachConversation,
|
||||
): Uint8Array<ArrayBuffer> {
|
||||
@@ -571,6 +605,15 @@ export interface CoachFactBackupEntry {
|
||||
category: string;
|
||||
confidence: number;
|
||||
sourceConversationId: string | null;
|
||||
/** v1.41 — `user`, `coach`, `extracted` or `pattern`. */
|
||||
source: string;
|
||||
/** v1.41 */
|
||||
lastUsedAt: string | null;
|
||||
/**
|
||||
* v1.41 — the message the fact came out of. A bare id like
|
||||
* `sourceConversationId`, resolved the same way on restore.
|
||||
*/
|
||||
sourceMessageId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
@@ -670,6 +713,9 @@ export async function buildCoachMemoryBackupSection(
|
||||
category: row.category,
|
||||
confidence: row.confidence,
|
||||
sourceConversationId: row.sourceConversationId,
|
||||
source: row.source,
|
||||
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
||||
sourceMessageId: row.sourceMessageId,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
})),
|
||||
@@ -726,6 +772,9 @@ const COACH_FACT_BACKUP_SELECT = {
|
||||
category: true,
|
||||
confidence: true,
|
||||
sourceConversationId: true,
|
||||
source: true,
|
||||
lastUsedAt: true,
|
||||
sourceMessageId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
@@ -795,6 +844,9 @@ export type RestoredCoachFact = Pick<
|
||||
| "fact"
|
||||
| "confidence"
|
||||
| "sourceConversationId"
|
||||
| "source"
|
||||
| "lastUsedAt"
|
||||
| "sourceMessageId"
|
||||
| "deletedAt"
|
||||
>
|
||||
>;
|
||||
@@ -859,12 +911,17 @@ export interface CoachMemoryRestoreInput {
|
||||
*
|
||||
* Plans go in before reminders for the same reason one step down: a reminder's
|
||||
* `relatedPlanId` is resolved against the plans this function has just written.
|
||||
*
|
||||
* A fact's `sourceMessageId` (v1.41) is resolved against the messages the
|
||||
* conversation restore wrote, and nulled and reported the same way when it
|
||||
* names one the file did not carry.
|
||||
*/
|
||||
export async function restoreCoachMemoryData(
|
||||
tx: Prisma.TransactionClient,
|
||||
ownerId: string,
|
||||
payload: CoachMemoryRestoreInput,
|
||||
conversationIds: ReadonlySet<string>,
|
||||
messageIds: ReadonlySet<string>,
|
||||
skips: RestoreSkipLog,
|
||||
): Promise<CoachMemoryRestoreCleared> {
|
||||
const [clearedFacts, clearedPlans, clearedReminders] = await Promise.all([
|
||||
@@ -884,6 +941,12 @@ export async function restoreCoachMemoryData(
|
||||
danglingRefs.push(id);
|
||||
return null;
|
||||
};
|
||||
const resolveMessage = (id: string | null | undefined) => {
|
||||
if (!id) return null;
|
||||
if (messageIds.has(id)) return id;
|
||||
danglingRefs.push(id);
|
||||
return null;
|
||||
};
|
||||
|
||||
if (payload.coachFacts.length > 0) {
|
||||
await tx.coachFact.createMany({
|
||||
@@ -894,6 +957,12 @@ export async function restoreCoachMemoryData(
|
||||
category: fact.category,
|
||||
confidence: fact.confidence ?? 50,
|
||||
sourceConversationId: resolveConversation(fact.sourceConversationId),
|
||||
// A file written before v1.41 states no source; every fact then came
|
||||
// from the extraction or the matcher, which is what the column's
|
||||
// default says too.
|
||||
source: fact.source ?? "extracted",
|
||||
lastUsedAt: fact.lastUsedAt ? new Date(fact.lastUsedAt) : null,
|
||||
sourceMessageId: resolveMessage(fact.sourceMessageId),
|
||||
createdAt: new Date(fact.createdAt),
|
||||
updatedAt: new Date(fact.updatedAt),
|
||||
deletedAt: fact.deletedAt ? new Date(fact.deletedAt) : null,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "@/lib/medications/course-window";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
import { DEFAULT_REASONING_MAX_EFFORT } from "@/lib/ai/reasoning/levels";
|
||||
import { prisma, toJson } from "@/lib/db";
|
||||
import { normaliseSourceInstance } from "@/lib/validations/inbound-documents";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -865,6 +866,11 @@ export async function restoreBackup(
|
||||
settings.assistantInsightStatusEnabled,
|
||||
assistantDocumentAiEnabled:
|
||||
settings.assistantDocumentAiEnabled ?? true,
|
||||
// v1.41 — absent on a file written before the controls existed,
|
||||
// which restores them at their defaults: allowed, no cap.
|
||||
aiReasoningEnabled: settings.aiReasoningEnabled ?? true,
|
||||
aiReasoningMaxEffort:
|
||||
settings.aiReasoningMaxEffort ?? DEFAULT_REASONING_MAX_EFFORT,
|
||||
moduleAvailabilityJson: coachFold.moduleAvailabilityJson as never,
|
||||
documentMaxFileBytes: settings.documentMaxFileBytes,
|
||||
documentQuotaBytes: BigInt(settings.documentQuotaBytes),
|
||||
@@ -2475,6 +2481,11 @@ export async function restoreBackup(
|
||||
ownerId,
|
||||
payload,
|
||||
new Set(payload.coachConversations.map((c) => c.id)),
|
||||
new Set(
|
||||
payload.coachConversations.flatMap((c) =>
|
||||
c.messages.map((m) => m.id),
|
||||
),
|
||||
),
|
||||
skips,
|
||||
);
|
||||
|
||||
|
||||
@@ -698,4 +698,21 @@ describe("a Coach turn's stored tables are bounded on restore", () => {
|
||||
const sealed = "A".repeat(300 * 1024);
|
||||
expect(parseWithResults({ resultsEncrypted: sealed }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("bounds the trail's text the same way, at its own 16 KB", () => {
|
||||
const fits = JSON.stringify({
|
||||
entries: [{ id: "a1", text: "x".repeat(15 * 1024) }],
|
||||
});
|
||||
expect(parseWithResults({ trailJson: fits }).success).toBe(true);
|
||||
expect(
|
||||
parseWithResults({ trailEncrypted: "A".repeat(30 * 1024) }).success,
|
||||
).toBe(true);
|
||||
const tooLong = JSON.stringify({
|
||||
entries: [{ id: "a1", text: "x".repeat(17 * 1024) }],
|
||||
});
|
||||
expect(parseWithResults({ trailJson: tooLong }).success).toBe(false);
|
||||
expect(
|
||||
parseWithResults({ trailEncrypted: "A".repeat(40 * 1024) }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DEFAULT_COACH_CLUSTERS,
|
||||
DEFAULT_COACH_PREFS,
|
||||
coachPrefsSchema,
|
||||
coachReasoningLevel,
|
||||
parseCoachPrefs,
|
||||
} from "../coach-prefs";
|
||||
|
||||
@@ -220,3 +221,39 @@ describe("parseCoachPrefs", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reasoning (v1.41)", () => {
|
||||
it("is absent on a legacy blob and reads as medium", () => {
|
||||
const prefs = parseCoachPrefs({ tone: "neutral" });
|
||||
expect(prefs).not.toHaveProperty("reasoning");
|
||||
expect(coachReasoningLevel(prefs)).toBe("medium");
|
||||
expect(coachReasoningLevel(DEFAULT_COACH_PREFS)).toBe("medium");
|
||||
});
|
||||
|
||||
it.each(["off", "low", "medium", "high"] as const)(
|
||||
"round-trips %s",
|
||||
(level) => {
|
||||
const prefs = parseCoachPrefs({ reasoning: level });
|
||||
expect(prefs.reasoning).toBe(level);
|
||||
expect(coachReasoningLevel(prefs)).toBe(level);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an unknown level on write", () => {
|
||||
expect(coachPrefsSchema.safeParse({ reasoning: "max" }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("drops an unknown stored level alone and keeps every other field", () => {
|
||||
const prefs = parseCoachPrefs({
|
||||
reasoning: "xhigh",
|
||||
tone: "concise",
|
||||
excludeMetrics: ["weight"],
|
||||
});
|
||||
expect(prefs.reasoning).toBeUndefined();
|
||||
expect(coachReasoningLevel(prefs)).toBe("medium");
|
||||
expect(prefs.tone).toBe("concise");
|
||||
expect(prefs.excludeMetrics).toEqual(["weight"]);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import { RESULTS_MAX_BYTES } from "@/lib/ai/coach/results/refs";
|
||||
import { TRAIL_MAX_BYTES } from "@/lib/ai/coach/activity/contract";
|
||||
import { REASONING_MAX_EFFORTS } from "@/lib/ai/reasoning/levels";
|
||||
import {
|
||||
AllergyCategory,
|
||||
AllergySeverity,
|
||||
@@ -1056,6 +1058,9 @@ const appSettingsBackupSchema = z
|
||||
// was retired still carries that key; `passthrough` keeps it harmless and
|
||||
// the restore writes nothing for it.
|
||||
assistantDocumentAiEnabled: z.boolean().optional(),
|
||||
// v1.41 — optional for the same reason.
|
||||
aiReasoningEnabled: z.boolean().optional(),
|
||||
aiReasoningMaxEffort: z.enum(REASONING_MAX_EFFORTS).optional(),
|
||||
moduleAvailabilityJson: z.unknown().nullable(),
|
||||
documentMaxFileBytes: z.number().int(),
|
||||
documentQuotaBytes: z.string().regex(/^\d+$/),
|
||||
@@ -1478,6 +1483,21 @@ const coachResultsSealedSchema = base64BytesSchema.refine(
|
||||
{ message: "Coach result tables exceed the stored size" },
|
||||
);
|
||||
|
||||
/** The trail's model text, bounded at restore like the tables above. */
|
||||
const COACH_TRAIL_SEALED_MAX_CHARS = 2 * TRAIL_MAX_BYTES;
|
||||
|
||||
const coachTrailJsonSchema = z
|
||||
.string()
|
||||
.refine(
|
||||
(value) => new TextEncoder().encode(value).byteLength <= TRAIL_MAX_BYTES,
|
||||
{ message: `Coach trail exceeds ${TRAIL_MAX_BYTES} bytes` },
|
||||
);
|
||||
|
||||
const coachTrailSealedSchema = base64BytesSchema.refine(
|
||||
(value) => value.length <= COACH_TRAIL_SEALED_MAX_CHARS,
|
||||
{ message: "Coach trail exceeds the stored size" },
|
||||
);
|
||||
|
||||
/**
|
||||
* One Coach turn.
|
||||
*
|
||||
@@ -1502,6 +1522,9 @@ const coachMessageBackupSchema = z
|
||||
// readable JSON on a portable one, absent on every file written before.
|
||||
resultsEncrypted: coachResultsSealedSchema.nullable().optional(),
|
||||
resultsJson: coachResultsJsonSchema.nullable().optional(),
|
||||
// v1.41 — the trail's model text, the same two ends as the tables.
|
||||
trailEncrypted: coachTrailSealedSchema.nullable().optional(),
|
||||
trailJson: coachTrailJsonSchema.nullable().optional(),
|
||||
createdAt: isoDateTime,
|
||||
})
|
||||
.passthrough();
|
||||
@@ -1549,6 +1572,11 @@ const coachFactBackupSchema = z
|
||||
category: z.string().min(1),
|
||||
confidence: z.number().int().min(0).max(100).optional(),
|
||||
sourceConversationId: z.string().nullable().optional(),
|
||||
// v1.41 — absent on every file written before; restored at the column
|
||||
// defaults then.
|
||||
source: z.string().min(1).max(32).optional(),
|
||||
lastUsedAt: isoDateTime.nullable().optional(),
|
||||
sourceMessageId: z.string().nullable().optional(),
|
||||
createdAt: isoDateTime,
|
||||
updatedAt: isoDateTime,
|
||||
deletedAt: isoDateTime.nullable().optional(),
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
DEFAULT_REASONING_LEVEL,
|
||||
REASONING_LEVELS,
|
||||
type ReasoningLevel,
|
||||
} from "@/lib/ai/reasoning/levels";
|
||||
|
||||
/**
|
||||
* Tone presets the Coach system-prompt prefix toggles between. v1.4.22
|
||||
* landed `warm` as the default; `neutral` strips the warmth language
|
||||
@@ -196,6 +202,16 @@ export const coachPrefsSchema = z.object({
|
||||
.describe(
|
||||
"v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false` switches them off.",
|
||||
),
|
||||
// v1.41 — how hard the Coach thinks before it answers. No schema default,
|
||||
// like the two keys above, so a legacy blob and `parse({})` stay
|
||||
// byte-identical; an absent key reads as `medium` (`coachReasoningLevel`).
|
||||
// The operator's switch and cap apply on top, server-side.
|
||||
reasoning: z
|
||||
.enum(REASONING_LEVELS)
|
||||
.optional()
|
||||
.describe(
|
||||
"v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means `medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET /api/auth/me` as `coachReasoning`.",
|
||||
),
|
||||
});
|
||||
|
||||
export type CoachPrefs = z.infer<typeof coachPrefsSchema>;
|
||||
@@ -214,6 +230,11 @@ export const DEFAULT_COACH_PREFS: CoachPrefs = {
|
||||
defaultWindow: "allTime",
|
||||
};
|
||||
|
||||
/** The person's reasoning level; an absent key is the default, `medium`. */
|
||||
export function coachReasoningLevel(prefs: CoachPrefs): ReasoningLevel {
|
||||
return prefs.reasoning ?? DEFAULT_REASONING_LEVEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a row's `coachPrefsJson` Json blob into a typed `CoachPrefs`,
|
||||
* falling back to defaults when the row is null. Keeps the call sites at
|
||||
|
||||
@@ -165,6 +165,8 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
assistantBriefingEnabled: false,
|
||||
assistantInsightStatusEnabled: false,
|
||||
assistantDocumentAiEnabled: false,
|
||||
aiReasoningEnabled: false,
|
||||
aiReasoningMaxEffort: "medium",
|
||||
moduleAvailabilityJson: { insights: true, nutrients: false },
|
||||
documentMaxFileBytes: 12_345_678,
|
||||
documentQuotaBytes: BigInt("9876543210"),
|
||||
|
||||
@@ -143,6 +143,10 @@ const PHQ9_RESPONSES = JSON.stringify({
|
||||
const CONSENT_ARTEFACT = "JVBERi0xLjQKJWZpeHR1cmU=";
|
||||
const INVENTORY_NOTE = "second pack, kept in the kitchen drawer";
|
||||
const COACH_FACT = "does not tolerate ACE inhibitors";
|
||||
// v1.41 — the model text of a turn's trail, stored encrypted beside the turn.
|
||||
const COACH_TRAIL_JSON = JSON.stringify({
|
||||
entries: [{ id: "a1", title: "Checking the trend", text: "Read 30 days." }],
|
||||
});
|
||||
const COACH_PLAN_CUE = "if the evening reading is over 140";
|
||||
const COACH_PLAN_ACTION = "then walk twenty minutes before dinner tomorrow";
|
||||
const COACH_REMINDER_NOTE = "ask how the evening walks are going";
|
||||
@@ -1092,6 +1096,7 @@ async function seedEveryTwoEndedModel(prisma: PrismaClient): Promise<void> {
|
||||
role: "assistant",
|
||||
encryptedContent: encryptToBytes(COACH_ASSISTANT_TURN),
|
||||
resultsEncrypted: encryptToBytes(COACH_RESULTS_JSON),
|
||||
trailEncrypted: encryptToBytes(COACH_TRAIL_JSON),
|
||||
providerType: "anthropic",
|
||||
model: "claude-opus-5",
|
||||
tokensUsed: 812,
|
||||
@@ -1131,6 +1136,9 @@ async function seedEveryTwoEndedModel(prisma: PrismaClient): Promise<void> {
|
||||
const healthThread = await prisma.coachConversation.findFirstOrThrow({
|
||||
where: { userId: OWNER_ID, documentScoped: false },
|
||||
});
|
||||
const healthAnswer = await prisma.coachMessage.findFirstOrThrow({
|
||||
where: { conversationId: healthThread.id, role: "assistant" },
|
||||
});
|
||||
await prisma.coachFact.create({
|
||||
data: {
|
||||
userId: OWNER_ID,
|
||||
@@ -1138,6 +1146,9 @@ async function seedEveryTwoEndedModel(prisma: PrismaClient): Promise<void> {
|
||||
category: "constraint",
|
||||
confidence: 80,
|
||||
sourceConversationId: healthThread.id,
|
||||
source: "user",
|
||||
lastUsedAt: AT("2026-07-20T10:00:00.000Z"),
|
||||
sourceMessageId: healthAnswer.id,
|
||||
},
|
||||
});
|
||||
const plan = await prisma.coachPlan.create({
|
||||
@@ -1768,6 +1779,9 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
results: message.resultsEncrypted
|
||||
? decryptFromBytes(message.resultsEncrypted)
|
||||
: null,
|
||||
trail: message.trailEncrypted
|
||||
? decryptFromBytes(message.trailEncrypted)
|
||||
: null,
|
||||
})),
|
||||
attachments: thread.attachments.map((a) => a.documentId),
|
||||
})),
|
||||
@@ -1786,6 +1800,7 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
model: null,
|
||||
tokensUsed: null,
|
||||
results: null,
|
||||
trail: null,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -1793,6 +1808,7 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
model: "claude-opus-5",
|
||||
tokensUsed: 812,
|
||||
results: COACH_RESULTS_JSON,
|
||||
trail: COACH_TRAIL_JSON,
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
@@ -1810,6 +1826,7 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
model: null,
|
||||
tokensUsed: null,
|
||||
results: null,
|
||||
trail: null,
|
||||
},
|
||||
],
|
||||
attachments: [vaultDocument.id],
|
||||
@@ -1830,17 +1847,26 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
const fact = await prisma.coachFact.findFirstOrThrow({
|
||||
where: { userId: OWNER_ID },
|
||||
});
|
||||
const restoredAnswer = await prisma.coachMessage.findFirstOrThrow({
|
||||
where: { conversationId: restoredThread.id, role: "assistant" },
|
||||
});
|
||||
expect({
|
||||
fact: decryptFromBytes(fact.factEncrypted),
|
||||
category: fact.category,
|
||||
confidence: fact.confidence,
|
||||
source: fact.sourceConversationId,
|
||||
origin: fact.source,
|
||||
lastUsedAt: fact.lastUsedAt?.toISOString(),
|
||||
sourceMessageId: fact.sourceMessageId,
|
||||
deletedAt: fact.deletedAt,
|
||||
}).toEqual({
|
||||
fact: COACH_FACT,
|
||||
category: "constraint",
|
||||
confidence: 80,
|
||||
source: restoredThread.id,
|
||||
origin: "user",
|
||||
lastUsedAt: "2026-07-20T10:00:00.000Z",
|
||||
sourceMessageId: restoredAnswer.id,
|
||||
deletedAt: null,
|
||||
});
|
||||
expect({
|
||||
@@ -3077,6 +3103,7 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
role: "assistant",
|
||||
encryptedContent: encryptToBytes(COACH_ASSISTANT_TURN),
|
||||
resultsEncrypted: encryptToBytes(COACH_RESULTS_JSON),
|
||||
trailEncrypted: encryptToBytes(COACH_TRAIL_JSON),
|
||||
createdAt: AT("2026-07-20T09:59:00.000Z"),
|
||||
},
|
||||
],
|
||||
@@ -3091,8 +3118,12 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
coachConversations: Array<{ messages: Array<Record<string, unknown>> }>;
|
||||
};
|
||||
const [turn] = coachConversations[0].messages;
|
||||
expect(turn).toMatchObject({ resultsJson: COACH_RESULTS_JSON });
|
||||
expect(turn).toMatchObject({
|
||||
resultsJson: COACH_RESULTS_JSON,
|
||||
trailJson: COACH_TRAIL_JSON,
|
||||
});
|
||||
expect(turn).not.toHaveProperty("resultsEncrypted");
|
||||
expect(turn).not.toHaveProperty("trailEncrypted");
|
||||
|
||||
await prisma.user.delete({ where: { id: OWNER_ID } });
|
||||
await createOwner(prisma);
|
||||
@@ -3120,6 +3151,8 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
expect(decryptFromBytes(restored.resultsEncrypted!)).toBe(
|
||||
COACH_RESULTS_JSON,
|
||||
);
|
||||
expect(restored.trailEncrypted).not.toBeNull();
|
||||
expect(decryptFromBytes(restored.trailEncrypted!)).toBe(COACH_TRAIL_JSON);
|
||||
});
|
||||
|
||||
it("drops and names a filing and a commitment a truncated file cannot resolve", async () => {
|
||||
|
||||
Reference in new issue
Block a user