Compare commits

...
Author SHA1 Message Date
m d8f994e07e DEV-000063 record completed production status
CI / test (push) Successful in 14s
CI / test (pull_request) Successful in 14s
2026-08-17 15:18:11 +03:00
m 9d50a3ec63 Merge DEV-000063 speech act follow-up
CI / test (push) Successful in 14s
Speech act classifier follow-up for DEV-000063.
2026-08-17 12:15:55 +00:00
m 2d142cdfdb DEV-000063 classify reminder and capability directives
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 19s
2026-08-17 15:14:56 +03:00
m 06b6cc3f9d Merge DEV-000063 Intent to Work Planning Foundation
CI / test (push) Successful in 20s
Promote DEV-000063 rev.1 to main.
2026-08-17 12:11:45 +00:00
m 4fc85511c4 DEV-000063 intent work planning foundation
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 14s
2026-08-17 15:10:43 +03:00
m 18750c2ae6 Merge DEV-000062 final production status
CI / test (push) Successful in 13s
Status-only completion record for DEV-000062.
2026-08-17 11:53:06 +00:00
m d1ce2c568e DEV-000062 record completed production status
CI / test (push) Successful in 14s
CI / test (pull_request) Successful in 13s
2026-08-17 14:51:37 +03:00
m 445e9edd7c Merge DEV-000062 focus question cognition fix
CI / test (push) Successful in 14s
Acceptance fix for DEV-000062 current-focus chat question.
2026-08-17 11:48:44 +00:00
m 48dfd364e4 DEV-000062 answer focus questions without cognition
CI / test (push) Successful in 14s
CI / test (pull_request) Successful in 14s
2026-08-17 14:47:13 +03:00
m e4021523b8 Merge DEV-000062 human intent evidence project context
CI / test (push) Successful in 13s
Promote DEV-000062 rev.1 to main.
2026-08-17 11:43:24 +00:00
m 790fcc0cea DEV-000062 human intent project context
CI / test (push) Successful in 15s
CI / test (pull_request) Successful in 14s
2026-08-17 14:42:24 +03:00
m 69ba990998 Merge pull request 'DEV-000061 production health alignment' (#32) from codex/dev000061-production-health-fix into main
CI / test (push) Successful in 14s
2026-08-17 11:24:00 +00:00
m 1918fb984c DEV-000061 align production health checks
CI / test (push) Successful in 13s
CI / test (pull_request) Successful in 13s
2026-08-17 14:23:11 +03:00
m 5bf97e2179 Merge pull request 'DEV-000061 interaction chat foundation' (#31) from codex/human-interaction-foundation-v1 into main
CI / test (push) Successful in 14s
2026-08-17 11:16:01 +00:00
31 changed files with 2067 additions and 41 deletions
@@ -43,6 +43,31 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
}
return tx.Commit(ctx)
}
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.RecordedAtUnixMillis); err != nil {
return err
}
for _, project := range projects {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.project_references(project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, project.ID, project.UtteranceID, project.ProjectID, project.ProjectDisplayName, project.Role, project.Provenance, project.ConfidenceMillis, project.RecordedAtUnixMillis); err != nil {
return err
}
}
for _, relation := range relations {
if relation.ToUtteranceID == "" {
continue
}
if _, err = tx.Exec(ctx, `INSERT INTO interaction.relations(relation_id,from_utterance_id,to_utterance_id,kind,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(relation_id) DO NOTHING`, relation.ID, relation.FromUtteranceID, relation.ToUtteranceID, relation.Kind, relation.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *InteractionRepository) Conversation(ctx context.Context, id interaction.ConversationID) (interaction.Conversation, error) {
var c interaction.Conversation
e := r.pool.QueryRow(ctx, `SELECT conversation_id,actor_id,principal_id,channel,state,created_at_unix_millis,updated_at_unix_millis FROM interaction.conversations WHERE conversation_id=$1`, id).Scan(&c.ID, &c.ActorID, &c.PrincipalID, &c.Channel, &c.State, &c.CreatedAtUnixMillis, &c.UpdatedAtUnixMillis)
+34
View File
@@ -3,7 +3,9 @@ package postgres
import (
"context"
"fmt"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/situation"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -39,3 +41,35 @@ func (r *SituationRepository) ByRuntime(ctx context.Context, runtime situation.R
}
return bindings, rows.Err()
}
func (r *SituationRepository) ListProjects(ctx context.Context) ([]situation.ProjectDescriptor, error) {
rows, err := r.pool.Query(ctx, `SELECT p.project_id, p.display_name, p.recorded_at_unix_millis, COALESCE(array_agg(a.alias ORDER BY a.alias) FILTER (WHERE a.alias IS NOT NULL), '{}') FROM situation.projects p LEFT JOIN situation.project_aliases a ON a.project_id=p.project_id GROUP BY p.project_id, p.display_name, p.recorded_at_unix_millis ORDER BY p.display_name`)
if err != nil {
return nil, err
}
defer rows.Close()
var projects []situation.ProjectDescriptor
for rows.Next() {
var project situation.ProjectDescriptor
var aliases []string
if err := rows.Scan(&project.ID, &project.DisplayName, &project.RecordedAtUnixMillis, &aliases); err != nil {
return nil, err
}
for _, alias := range aliases {
project.Aliases = append(project.Aliases, situation.ProjectAlias(strings.ToLower(alias)))
}
projects = append(projects, project)
}
return projects, rows.Err()
}
func (r *SituationRepository) ActiveProjectFocus(ctx context.Context, actorID identity.ActorID, principalID identity.PrincipalID) (situation.ActiveProjectFocus, error) {
var focus situation.ActiveProjectFocus
err := r.pool.QueryRow(ctx, `SELECT actor_id,principal_id,project_id,project_display_name,provenance,confidence_millis,source_utterance_id,updated_at_unix_millis FROM situation.active_project_focus WHERE actor_id=$1 AND principal_id=$2`, actorID, principalID).Scan(&focus.ActorID, &focus.PrincipalID, &focus.ProjectID, &focus.ProjectDisplayName, &focus.Provenance, &focus.ConfidenceMillis, &focus.SourceUtteranceID, &focus.UpdatedAtUnixMillis)
return focus, err
}
func (r *SituationRepository) SaveActiveProjectFocus(ctx context.Context, focus situation.ActiveProjectFocus) error {
_, err := r.pool.Exec(ctx, `INSERT INTO situation.active_project_focus(actor_id,principal_id,project_id,project_display_name,provenance,confidence_millis,source_utterance_id,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(actor_id,principal_id) DO UPDATE SET project_id=EXCLUDED.project_id, project_display_name=EXCLUDED.project_display_name, provenance=EXCLUDED.provenance, confidence_millis=EXCLUDED.confidence_millis, source_utterance_id=EXCLUDED.source_utterance_id, updated_at_unix_millis=EXCLUDED.updated_at_unix_millis`, focus.ActorID, focus.PrincipalID, focus.ProjectID, focus.ProjectDisplayName, focus.Provenance, focus.ConfidenceMillis, focus.SourceUtteranceID, focus.UpdatedAtUnixMillis)
return err
}
+36
View File
@@ -39,3 +39,39 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
}
return target, nil
}
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
goal := result.Goal
if _, err = tx.Exec(ctx, `INSERT INTO work.goals(goal_id,source_intent_id,source_utterance_id,actor_id,principal_id,scope_id,project_id,project_display_name,desired_outcome,constraints_text,state,provenance,revision,created_at_unix_millis,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(goal_id) DO NOTHING`, goal.ID, goal.SourceIntentID, goal.SourceUtteranceID, goal.ActorID, goal.PrincipalID, goal.ScopeID, goal.ProjectID, goal.ProjectDisplayName, goal.DesiredOutcome, goal.Constraints, goal.State, goal.Provenance, goal.Revision, goal.CreatedAtUnixMillis, goal.UpdatedAtUnixMillis); err != nil {
return err
}
candidate := result.Candidate
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_candidates(plan_candidate_id,goal_id,source,state,summary,validation_summary,requires_clarification,accepted_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT(plan_candidate_id) DO NOTHING`, candidate.ID, candidate.GoalID, candidate.Source, candidate.State, candidate.Summary, candidate.ValidationSummary, candidate.RequiresClarification, candidate.AcceptedAtUnixMillis, candidate.RecordedAtUnixMillis); err != nil {
return err
}
plan := result.Plan
if _, err = tx.Exec(ctx, `INSERT INTO work.work_plans(work_plan_id,goal_id,accepted_candidate_id,revision,state,summary,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(work_plan_id) DO NOTHING`, plan.ID, plan.GoalID, plan.AcceptedCandidateID, plan.Revision, plan.State, plan.Summary, plan.RecordedAtUnixMillis); err != nil {
return err
}
for _, requirement := range candidate.Requirements {
if _, err = tx.Exec(ctx, `INSERT INTO work.capability_requirements(capability_requirement_id,plan_candidate_id,capability_kind,capability_id,purpose,state,epistemic_precondition,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(capability_requirement_id) DO NOTHING`, requirement.ID, requirement.PlanCandidateID, requirement.CapabilityKind, requirement.CapabilityID, requirement.Purpose, requirement.State, requirement.EpistemicPrecondition, requirement.RecordedAtUnixMillis); err != nil {
return err
}
}
for _, step := range candidate.Steps {
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_steps(plan_step_id,plan_candidate_id,ordinal,kind,description,requires_capability_id,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(plan_step_id) DO NOTHING`, step.ID, step.PlanCandidateID, step.Ordinal, step.Kind, step.Description, step.RequiresCapabilityID, step.RecordedAtUnixMillis); err != nil {
return err
}
}
for _, delegation := range candidate.Delegations {
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_candidates(delegation_candidate_id,plan_candidate_id,capability_requirement_id,kind,executor_hint,state,reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(delegation_candidate_id) DO NOTHING`, delegation.ID, delegation.PlanCandidateID, delegation.CapabilityRequirementID, delegation.Kind, delegation.ExecutorHint, delegation.State, delegation.Reason, delegation.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
@@ -0,0 +1,22 @@
# ADR-0011 - Human Intent And Project Context Semantics
Status: Accepted
Date: 2026-08-17
Accepted under: DEV-000062 milestone decision
## Decision
DEV-000062 extends existing owners rather than creating a new canonical owner.
`interaction` owns typed, append-only interpretations of human utterances: speech-act confidence, original intent linkage, project reference candidates, correction/supersession relations, and whether a directive/decision/assertion is only a human semantic record. Interaction does not own truth, authority, execution, provider choice, or project canonical state.
`situation` owns the minimal V1 Project Registry and active project focus needed for human chat context resolution. It may store human-readable project aliases and per-actor/principal active focus references. Project resolution confidence and provenance are not Authority.
`epistemics` remains the owner of claims, observations, evidence and verified beliefs. A human assertion recorded through Interaction is not a verified fact. It may become input to a future Epistemics proposal only after a trusted observation/evidence path exists.
`work` remains the owner of durable Decisions/Tasks. DEV-000062 may record an explicit human decision/intention in Interaction, but it does not create a Work Decision or executable task unless a later accepted contract routes through Work/Authority/Commit.
## Consequences
The chat runtime can show a human-readable active project focus and persist semantic interpretation without direct mutation authority. Corrections are append-only. Multiple project references are represented as typed references, not copied project state. Cognitive help, when used, must remain behind the existing Cognitive Gateway/Router and produce validated candidates before persistence.
@@ -0,0 +1,43 @@
# ADR-0012 - Intent To Work Planning Foundation
Status: Accepted
Date: 2026-08-17
Accepted under: DEV-000063 milestone decision
## Decision
`work` remains the canonical owner for Goal, Task, Decision and the new V1 planning foundation. No new canonical owner is introduced.
Help is the central user-facing concept. Goal, Work and Plan are optional durable coordination primitives, not mandatory records for every conversation. A simple explanation, ordinary answer, short cognitive response, direct reminder capability, safe deterministic operation, or capability-gap response may bypass durable Work entirely.
Human chat must not reduce Domovoy to `chat -> prompt -> model` or to a Codex-specific task generator. The canonical pipeline is:
When durable coordination is actually needed, the canonical path is:
`HumanUtterance -> OriginalHumanIntent -> Goal -> PlanCandidate -> accepted Work/Plan state -> CapabilityRequirement -> DelegationCandidate/Action/Investigation/Observation/Outcome`.
Prompts, Codex instructions, mail payloads, reminders and provider requests are executor-adapter representations. They are not canonical human intent, Goal or Plan.
## Ownership
- `interaction` continues to own append-only human utterances, original intent provenance and typed utterance interpretations.
- `work` owns durable Goals, lifecycle, planning state, plan candidates, accepted plan summaries, capability requirements and general delegation candidates.
- `capability` owns available capability/binding registry and health. Work may reference capability requirements by typed IDs/kinds but does not select a concrete provider as canonical truth.
- `epistemics` owns verified/unverified/unknown evidence semantics. Work planning must preserve epistemic state and may add verification requirements before risky work.
- `authority` and `commit` remain required for any real mutation or external side effect. Planning creates no permission and no execution claim.
## Boundaries
Work PlanCandidates are structured candidates that must be validated before persistence or acceptance. Cognitive planning may later propose candidates only through the Cognitive Gateway/Router; model output never directly mutates canonical Work state. DEV-000063 may implement deterministic V1 candidate planning and keep cognitive candidate generation as a typed extension point.
CapabilityRequirement is executor-neutral. Examples include `READ_EMAIL`, `SEND_EMAIL`, `READ_DATABASE`, `QUERY_LOGS`, `INSPECT_PROJECT`, `MODIFY_CODE`, `RUN_TESTS`, `WEB_RESEARCH`, `COGNITIVE_REASONING`, and `SCHEDULE_REMINDER`. This is an extensible vocabulary, not a closed ontology and not an executor list.
DelegationCandidate is general. `Codex` is only a possible future implementation for `MODIFY_CODE`; mail, research, scheduler, device and database adapters can use the same canonical Work model without changing OriginalHumanIntent or Goal.
## Consequences
`/chat` can choose a HelpPath first. Only the durable-work path creates a planning-only Goal and Plan. Other paths can answer, use cognition, report a missing capability, or describe a direct capability requirement without materializing a Goal/Plan. It must not pretend work was completed. Completion remains tied to outcome semantics, not to a model response.
Project is only one possible context reference. The Work foundation stores project context as optional opaque references and must later permit people, emails, documents, images, servers, organizations, research subjects and other context types without redesigning Goal/Plan.
Capability gaps are first-class planning information. "I cannot do this yet" is not necessarily terminal in future milestones: the system may later discover, connect, configure or build a missing capability and return to the original need. DEV-000063 does not implement self-extension; it preserves the extension path.
+16 -4
View File
@@ -40,7 +40,9 @@
"id": "mod.situation",
"owns": [
"Situation",
"FocusBinding"
"FocusBinding",
"ProjectRegistry",
"ActiveProjectFocus"
],
"database_schema": "situation",
"boundary_class": "remote_capable",
@@ -55,14 +57,24 @@
"id": "mod.work",
"owns": [
"Goal",
"HelpPath",
"GoalLifecycle",
"Task",
"Decision"
"Decision",
"PlanCandidate",
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
],
"database_schema": "work",
"boundary_class": "remote_capable",
"may_depend_on": [
"contracts.identity",
"contracts.authority",
"contracts.epistemics",
"contracts.capability",
"platform.ids",
"platform.events"
]
},
@@ -144,10 +156,10 @@
},
"interaction": {
"id": "mod.interaction",
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation"],
"owns": ["Conversation", "HumanUtterance", "UtteranceInterpretation", "OriginalHumanIntent", "InteractionRelation", "ProjectReference"],
"database_schema": "interaction",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.cognitive", "platform.ids", "platform.events"]
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
}
},
"platform": {
+5 -5
View File
@@ -20,17 +20,17 @@ modules:
situation:
id: mod.situation
owns: [Situation, FocusBinding]
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
database_schema: situation
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
work:
id: mod.work
owns: [Goal, Task, Decision]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.events]
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
epistemics:
id: mod.epistemics
@@ -70,10 +70,10 @@ modules:
interaction:
id: mod.interaction
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation]
owns: [Conversation, HumanUtterance, UtteranceInterpretation, OriginalHumanIntent, InteractionRelation, ProjectReference]
database_schema: interaction
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.situation, contracts.cognitive, platform.ids, platform.events]
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
platform:
allowed_packages:
+89 -3
View File
@@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
@@ -16,6 +18,7 @@ import (
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
"github.com/jackc/pgx/v5"
@@ -201,17 +204,100 @@ func main() {
http.Error(w, "invalid chat message", http.StatusBadRequest)
return
}
projects, err := repository.ListProjects(r.Context())
if err != nil {
http.Error(w, "project registry unavailable", http.StatusServiceUnavailable)
return
}
var previousFocus *situation.ActiveProjectFocus
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
if err == nil {
previousFocus = &storedFocus
} else if !errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
return
}
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
if err != nil {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
http.Error(w, "save chat message", http.StatusInternalServerError)
return
}
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
activeFocusName := ""
activeFocusProvenance := situation.ProjectResolutionProvenance("")
if semantic.ActiveFocus != nil {
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
http.Error(w, "save project focus", http.StatusInternalServerError)
return
}
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
activeFocusProvenance = semantic.ActiveFocus.Provenance
} else if previousFocus != nil {
activeFocusName = previousFocus.ProjectDisplayName
activeFocusProvenance = previousFocus.Provenance
}
var planningReply string
workPlanned := false
needsCognition := semantic.NeedsCognition
projectID := ""
if semantic.ActiveFocus != nil {
projectID = string(semantic.ActiveFocus.ProjectID)
} else if previousFocus != nil {
projectID = string(previousFocus.ProjectID)
}
planningInput := work.PlanningInput{
ConversationID: work.ConversationRef(conversation.ID),
UtteranceID: work.SourceUtteranceRef(utterance.ID),
IntentID: work.SourceIntentRef(intent.ID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
ProjectID: work.ProjectRef(projectID),
ProjectDisplayName: activeFocusName,
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
Message: utterance.Content,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
if helpPath.NeedsDurableWork {
planning, err := workdomain.BuildPlanningResult(planningInput, now)
if err != nil {
http.Error(w, "plan chat work", http.StatusServiceUnavailable)
return
}
if err := workRepository.SavePlanningResult(r.Context(), planning); err != nil {
http.Error(w, "save work plan", http.StatusInternalServerError)
return
}
planningReply = planning.Reply
workPlanned = true
needsCognition = false
}
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
SpeechAct: utterance.SpeechAct,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: interactiondomain.NeedsCognition(utterance.SpeechAct),
FallbackText: interactiondomain.FallbackText(utterance.SpeechAct),
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
+14 -7
View File
@@ -142,11 +142,15 @@ type browserChatRequest struct {
}
type browserChatResponse struct {
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -313,7 +317,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
reply := saved.FallbackText
cognition := "not_requested"
if saved.NeedsCognition {
if saved.PlanningReply != "" {
reply = saved.PlanningReply
}
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
@@ -333,7 +340,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition})
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned})
}
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
+2 -1
View File
@@ -32,7 +32,8 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; }
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
.chat-header span { margin-left: auto; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
#active-focus { margin-left: auto; color: var(--good); }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
+12 -1
View File
@@ -7,6 +7,7 @@ const chatForm = document.querySelector("#chat-form");
const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
@@ -38,6 +39,11 @@ function appendMessage(kind, text, meta = "") {
messages.scrollTop = messages.scrollHeight;
}
function updateActiveFocus(value, provenance = "") {
if (!activeFocus) return;
activeFocus.textContent = value ? `фокус: ${value}${provenance ? ` · ${provenance}` : ""}` : "фокус: не выбран";
}
async function api(path, options = {}) {
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
if (token) headers.Authorization = `Bearer ${token}`;
@@ -72,7 +78,11 @@ chatForm.addEventListener("submit", async (event) => {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: text})});
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
appendMessage("assistant", result.reply_text || "Сообщение записано.", `${result.speech_act}; cognition=${result.cognition}`);
updateActiveFocus(result.active_focus, result.project_provenance);
const semanticMeta = [result.speech_act, `cognition=${result.cognition}`];
if (result.work_planned) semanticMeta.push("work=planned");
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} finally {
@@ -87,6 +97,7 @@ logoutButton.addEventListener("click", () => {
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
showLogin();
});
+1
View File
@@ -26,6 +26,7 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/" aria-label="Консоль создателя">Домовой</a>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
<button id="logout" type="button" aria-label="Выйти">↗</button>
</header>
+13
View File
@@ -7,6 +7,7 @@ import (
type CapabilityID ids.ID
type CapabilityBindingID ids.ID
type CapabilityKind string
type BindingState string
type BindingProtocol string
@@ -16,6 +17,18 @@ const (
BindingRetired BindingState = "RETIRED"
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
CapabilityKindSendEmail CapabilityKind = "SEND_EMAIL"
CapabilityKindReadDatabase CapabilityKind = "READ_DATABASE"
CapabilityKindQueryLogs CapabilityKind = "QUERY_LOGS"
CapabilityKindInspectProject CapabilityKind = "INSPECT_PROJECT"
CapabilityKindModifyCode CapabilityKind = "MODIFY_CODE"
CapabilityKindRunTests CapabilityKind = "RUN_TESTS"
CapabilityKindWebResearch CapabilityKind = "WEB_RESEARCH"
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
)
// Binding is Capability-owned correctness state. SecretRef is an opaque
+13
View File
@@ -5,3 +5,16 @@ import "git.cin.su/m/Assistant/platform/ids"
type ObservationID ids.ID
type ClaimID ids.ID
type BeliefID ids.ID
type VerificationState string
type ClaimSourceKind string
const (
VerificationUnverified VerificationState = "UNVERIFIED"
VerificationVerified VerificationState = "VERIFIED"
VerificationRejected VerificationState = "REJECTED"
VerificationUnknown VerificationState = "UNKNOWN"
VerificationNotApplicable VerificationState = "NOT_APPLICABLE"
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
ClaimSourceTrustedObservation ClaimSourceKind = "TRUSTED_OBSERVATION"
)
+43 -1
View File
@@ -1,6 +1,9 @@
package interaction
import "git.cin.su/m/Assistant/contracts/identity"
import (
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/situation"
)
type ConversationID string
type HumanUtteranceID string
@@ -11,6 +14,10 @@ type Channel string
type ConversationState string
type InterpretationState string
type InteractionRelationKind string
type InterpretationID string
type ProjectReferenceID string
type ProjectReferenceRole string
type ClarificationState string
const (
ChannelWebChat Channel = "WEB_CHAT"
@@ -35,6 +42,13 @@ const (
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
)
type Conversation struct {
@@ -74,6 +88,29 @@ type InteractionRelation struct {
RecordedAtUnixMillis int64
}
type UtteranceInterpretation struct {
ID InterpretationID
UtteranceID HumanUtteranceID
SpeechAct SpeechAct
ConfidenceMillis int64
Source string
AssertionVerification string
RequestedOutcome string
Clarification ClarificationState
RecordedAtUnixMillis int64
}
type ProjectReference struct {
ID ProjectReferenceID
UtteranceID HumanUtteranceID
ProjectID situation.ProjectID
ProjectDisplayName string
Role ProjectReferenceRole
Provenance situation.ProjectResolutionProvenance
ConfidenceMillis int64
RecordedAtUnixMillis int64
}
type ChatMessageRequest struct {
ConversationID ConversationID
Message string
@@ -88,4 +125,9 @@ type ChatMessageResponse struct {
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
}
+31
View File
@@ -12,6 +12,19 @@ type FocusBindingID ids.ID
type SessionID ids.ID
type WorkspaceID ids.ID
type ProjectID ids.ID
type ProjectAlias string
type ProjectResolutionProvenance string
const (
ProjectResolutionExplicit ProjectResolutionProvenance = "EXPLICIT"
ProjectResolutionInheritedFocus ProjectResolutionProvenance = "INHERITED_FOCUS"
ProjectResolutionEntityReference ProjectResolutionProvenance = "ENTITY_REFERENCE"
ProjectResolutionTaskReference ProjectResolutionProvenance = "TASK_REFERENCE"
ProjectResolutionRepositoryReference ProjectResolutionProvenance = "REPOSITORY_REFERENCE"
ProjectResolutionInferred ProjectResolutionProvenance = "INFERRED"
ProjectResolutionUserConfirmed ProjectResolutionProvenance = "USER_CONFIRMED"
ProjectResolutionAmbiguous ProjectResolutionProvenance = "AMBIGUOUS"
)
// FocusBinding is the situation-owned runtime context used to resolve a request.
// User text is deliberately absent: textual similarity is not a routing signal.
@@ -30,3 +43,21 @@ type RuntimeBinding struct {
SessionID SessionID
Workspace WorkspaceID
}
type ProjectDescriptor struct {
ID ProjectID
DisplayName string
Aliases []ProjectAlias
RecordedAtUnixMillis int64
}
type ActiveProjectFocus struct {
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ProjectID ProjectID
ProjectDisplayName string
Provenance ProjectResolutionProvenance
ConfidenceMillis int64
SourceUtteranceID string
UpdatedAtUnixMillis int64
}
+184
View File
@@ -2,13 +2,88 @@ package work
import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type GoalID ids.ID
type TaskID ids.ID
type DecisionID ids.ID
type PlanCandidateID ids.ID
type WorkPlanID ids.ID
type PlanStepID ids.ID
type CapabilityRequirementID ids.ID
type DelegationCandidateID ids.ID
type TaskRevision uint64
type GoalRevision uint64
type PlanRevision uint64
type GoalState string
type PlanCandidateState string
type PlanStepKind string
type CapabilityRequirementState string
type DelegationCandidateState string
type DelegationKind string
type SourceIntentRef string
type SourceUtteranceRef string
type ConversationRef string
type ProjectRef string
type ProjectProvenanceRef string
type SpeechActRef string
type HelpPathKind string
const (
GoalProposed GoalState = "PROPOSED"
GoalUnderstood GoalState = "UNDERSTOOD"
GoalPlanning GoalState = "PLANNING"
GoalReady GoalState = "READY"
GoalInProgress GoalState = "IN_PROGRESS"
GoalBlocked GoalState = "BLOCKED"
GoalCompleted GoalState = "COMPLETED"
GoalFailed GoalState = "FAILED"
GoalCancelled GoalState = "CANCELLED"
GoalSuperseded GoalState = "SUPERSEDED"
PlanCandidateProposed PlanCandidateState = "PROPOSED"
PlanCandidateValidated PlanCandidateState = "VALIDATED"
PlanCandidateAccepted PlanCandidateState = "ACCEPTED"
PlanCandidateRejected PlanCandidateState = "REJECTED"
PlanStepInvestigate PlanStepKind = "INVESTIGATE"
PlanStepObserve PlanStepKind = "OBSERVE"
PlanStepVerify PlanStepKind = "VERIFY"
PlanStepReason PlanStepKind = "REASON"
PlanStepReport PlanStepKind = "REPORT"
PlanStepPrepare PlanStepKind = "PREPARE"
PlanStepCommunicate PlanStepKind = "COMMUNICATE"
PlanStepSchedule PlanStepKind = "SCHEDULE"
PlanStepModify PlanStepKind = "MODIFY"
PlanStepRunChecks PlanStepKind = "RUN_CHECKS"
PlanStepClarify PlanStepKind = "CLARIFY"
PlanStepNoExecution PlanStepKind = "NO_EXECUTION"
CapabilityRequirementRequired CapabilityRequirementState = "REQUIRED"
CapabilityRequirementAvailable CapabilityRequirementState = "AVAILABLE"
CapabilityRequirementMissing CapabilityRequirementState = "MISSING"
CapabilityRequirementBlocked CapabilityRequirementState = "BLOCKED"
DelegationCandidateProposed DelegationCandidateState = "PROPOSED"
DelegationCandidateSelected DelegationCandidateState = "SELECTED"
DelegationCandidateBlocked DelegationCandidateState = "BLOCKED"
DelegationKindAgent DelegationKind = "AGENT"
DelegationKindTool DelegationKind = "TOOL"
DelegationKindAdapter DelegationKind = "ADAPTER"
DelegationKindHuman DelegationKind = "HUMAN"
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
HelpPathAnswer HelpPathKind = "ANSWER"
HelpPathCognition HelpPathKind = "COGNITION"
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
HelpPathDurableWork HelpPathKind = "DURABLE_WORK"
HelpPathCapabilityGap HelpPathKind = "CAPABILITY_GAP"
)
// TaskTarget carries the integrity context that a work-owned mutation must match.
type TaskTarget struct {
@@ -17,3 +92,112 @@ type TaskTarget struct {
ScopeID authority.ScopeID
Revision TaskRevision
}
type Goal struct {
ID GoalID
SourceIntentID SourceIntentRef
SourceUtteranceID SourceUtteranceRef
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
ProjectID ProjectRef
ProjectDisplayName string
DesiredOutcome string
Constraints string
State GoalState
Provenance ProjectProvenanceRef
Revision GoalRevision
CreatedAtUnixMillis int64
UpdatedAtUnixMillis int64
}
type CapabilityRequirement struct {
ID CapabilityRequirementID
PlanCandidateID PlanCandidateID
CapabilityKind capability.CapabilityKind
CapabilityID capability.CapabilityID
Purpose string
State CapabilityRequirementState
EpistemicPrecondition epistemics.VerificationState
RecordedAtUnixMillis int64
}
type PlanStep struct {
ID PlanStepID
PlanCandidateID PlanCandidateID
Ordinal int
Kind PlanStepKind
Description string
RequiresCapabilityID CapabilityRequirementID
RecordedAtUnixMillis int64
}
type DelegationCandidate struct {
ID DelegationCandidateID
PlanCandidateID PlanCandidateID
CapabilityRequirementID CapabilityRequirementID
Kind DelegationKind
ExecutorHint string
State DelegationCandidateState
Reason string
RecordedAtUnixMillis int64
}
type PlanCandidate struct {
ID PlanCandidateID
GoalID GoalID
Source string
State PlanCandidateState
Summary string
ValidationSummary string
RequiresClarification bool
AcceptedAtUnixMillis int64
RecordedAtUnixMillis int64
Steps []PlanStep
Requirements []CapabilityRequirement
Delegations []DelegationCandidate
}
type WorkPlan struct {
ID WorkPlanID
GoalID GoalID
AcceptedCandidateID PlanCandidateID
Revision PlanRevision
State PlanCandidateState
Summary string
RecordedAtUnixMillis int64
}
type PlanningInput struct {
ConversationID ConversationRef
UtteranceID SourceUtteranceRef
IntentID SourceIntentRef
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
ProjectID ProjectRef
ProjectDisplayName string
ProjectProvenance ProjectProvenanceRef
Message string
SpeechAct SpeechActRef
AssertionVerification epistemics.VerificationState
ReceivedAtUnixMillis int64
}
type PlanningResult struct {
Goal Goal
Candidate PlanCandidate
Plan WorkPlan
Reply string
}
type HelpPath struct {
Kind HelpPathKind
NeedsDurableGoal bool
NeedsDurableWork bool
NeedsDurablePlan bool
SuggestedCapabilityKinds []capability.CapabilityKind
CapabilityGap bool
Reason string
Reply string
}
+159 -1
View File
@@ -135,6 +135,97 @@ CREATE TABLE IF NOT EXISTS work.tasks (
task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL,
revision BIGINT NOT NULL, CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.goals (
goal_id TEXT PRIMARY KEY,
source_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
project_display_name TEXT NOT NULL DEFAULT '',
desired_outcome TEXT NOT NULL,
constraints_text TEXT NOT NULL,
state TEXT NOT NULL,
provenance TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (desired_outcome <> ''),
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.plan_candidates (
plan_candidate_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
source TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
validation_summary TEXT NOT NULL,
requires_clarification BOOLEAN NOT NULL,
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (source <> ''),
CHECK (summary <> ''),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.work_plans (
work_plan_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
accepted_candidate_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (accepted_candidate_id <> ''),
CHECK (revision > 0),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.capability_requirements (
capability_requirement_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
purpose TEXT NOT NULL,
state TEXT NOT NULL,
epistemic_precondition TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_kind <> ''),
CHECK (purpose <> ''),
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
);
CREATE TABLE IF NOT EXISTS work.plan_steps (
plan_step_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
kind TEXT NOT NULL,
description TEXT NOT NULL,
requires_capability_id TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (ordinal > 0),
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
CHECK (description <> '')
);
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
delegation_candidate_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_requirement_id TEXT NOT NULL,
kind TEXT NOT NULL,
executor_hint TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL,
reason TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_requirement_id <> ''),
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
CHECK (reason <> '')
);
RESET ROLE;
SET ROLE situation_role;
@@ -149,6 +240,45 @@ CREATE TABLE IF NOT EXISTS situation.focus_bindings (
active BOOLEAN NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS situation.projects (
project_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (project_id <> ''),
CHECK (display_name <> '')
);
CREATE TABLE IF NOT EXISTS situation.project_aliases (
project_id TEXT NOT NULL,
alias TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (project_id, alias),
CHECK (project_id <> ''),
CHECK (alias <> '')
);
CREATE TABLE IF NOT EXISTS situation.active_project_focus (
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
project_id TEXT NOT NULL,
project_display_name TEXT NOT NULL,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
source_utterance_id TEXT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (actor_id, principal_id),
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
CHECK (confidence_millis BETWEEN 0 AND 1000)
);
INSERT INTO situation.projects(project_id, display_name, recorded_at_unix_millis)
VALUES ('assistant', 'Assistant', 1), ('upo', 'УПО', 1)
ON CONFLICT (project_id) DO NOTHING;
INSERT INTO situation.project_aliases(project_id, alias, recorded_at_unix_millis)
VALUES
('assistant', 'assistant', 1),
('assistant', 'домовой', 1),
('assistant', 'ассистент', 1),
('upo', 'упо', 1),
('upo', 'upo', 1)
ON CONFLICT (project_id, alias) DO NOTHING;
RESET ROLE;
SET ROLE capability_role;
@@ -352,7 +482,35 @@ CREATE TABLE IF NOT EXISTS interaction.relations (
to_utterance_id TEXT NOT NULL,
kind TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK'))
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK','SUPERSEDES','CLARIFIES'))
);
CREATE TABLE IF NOT EXISTS interaction.utterance_interpretations (
interpretation_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
speech_act TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
source TEXT NOT NULL,
assertion_verification TEXT NOT NULL,
requested_outcome TEXT NOT NULL DEFAULT '',
clarification TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (confidence_millis BETWEEN 0 AND 1000),
CHECK (assertion_verification IN ('UNVERIFIED','VERIFIED','REJECTED','NOT_APPLICABLE')),
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
);
CREATE TABLE IF NOT EXISTS interaction.project_references (
project_reference_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
project_id TEXT NOT NULL,
project_display_name TEXT NOT NULL,
role TEXT NOT NULL,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (role IN ('PRIMARY','RELATED','REFERENCE_SOURCE')),
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
CHECK (confidence_millis BETWEEN 0 AND 1000)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,89 @@
BEGIN;
SET ROLE situation_role;
CREATE TABLE IF NOT EXISTS situation.projects (
project_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (project_id <> ''),
CHECK (display_name <> '')
);
CREATE TABLE IF NOT EXISTS situation.project_aliases (
project_id TEXT NOT NULL,
alias TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (project_id, alias),
CHECK (project_id <> ''),
CHECK (alias <> '')
);
CREATE TABLE IF NOT EXISTS situation.active_project_focus (
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
project_id TEXT NOT NULL,
project_display_name TEXT NOT NULL,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
source_utterance_id TEXT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (actor_id, principal_id),
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
CHECK (confidence_millis BETWEEN 0 AND 1000)
);
INSERT INTO situation.projects(project_id, display_name, recorded_at_unix_millis)
VALUES ('assistant', 'Assistant', 1), ('upo', 'УПО', 1)
ON CONFLICT (project_id) DO NOTHING;
INSERT INTO situation.project_aliases(project_id, alias, recorded_at_unix_millis)
VALUES
('assistant', 'assistant', 1),
('assistant', 'домовой', 1),
('assistant', 'ассистент', 1),
('upo', 'упо', 1),
('upo', 'upo', 1)
ON CONFLICT (project_id, alias) DO NOTHING;
RESET ROLE;
SET ROLE interaction_role;
ALTER TABLE interaction.relations DROP CONSTRAINT IF EXISTS relations_kind_check;
ALTER TABLE interaction.relations
ADD CONSTRAINT relations_kind_check
CHECK (kind IN ('CORRECTS','RESPONDS_TO','REQUESTS_COGNITION','COGNITION_FALLBACK','SUPERSEDES','CLARIFIES'));
CREATE TABLE IF NOT EXISTS interaction.utterance_interpretations (
interpretation_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
speech_act TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
source TEXT NOT NULL,
assertion_verification TEXT NOT NULL,
requested_outcome TEXT NOT NULL DEFAULT '',
clarification TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (confidence_millis BETWEEN 0 AND 1000),
CHECK (assertion_verification IN ('UNVERIFIED','VERIFIED','REJECTED','NOT_APPLICABLE')),
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
);
CREATE TABLE IF NOT EXISTS interaction.project_references (
project_reference_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
project_id TEXT NOT NULL,
project_display_name TEXT NOT NULL,
role TEXT NOT NULL,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (role IN ('PRIMARY','RELATED','REFERENCE_SOURCE')),
CHECK (provenance IN ('EXPLICIT','INHERITED_FOCUS','ENTITY_REFERENCE','TASK_REFERENCE','REPOSITORY_REFERENCE','INFERRED','USER_CONFIRMED','AMBIGUOUS')),
CHECK (confidence_millis BETWEEN 0 AND 1000)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,104 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.goals (
goal_id TEXT PRIMARY KEY,
source_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
project_display_name TEXT NOT NULL DEFAULT '',
desired_outcome TEXT NOT NULL,
constraints_text TEXT NOT NULL,
state TEXT NOT NULL,
provenance TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (desired_outcome <> ''),
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.plan_candidates (
plan_candidate_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
source TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
validation_summary TEXT NOT NULL,
requires_clarification BOOLEAN NOT NULL,
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (source <> ''),
CHECK (summary <> ''),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.work_plans (
work_plan_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
accepted_candidate_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (accepted_candidate_id <> ''),
CHECK (revision > 0),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.capability_requirements (
capability_requirement_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
purpose TEXT NOT NULL,
state TEXT NOT NULL,
epistemic_precondition TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_kind <> ''),
CHECK (purpose <> ''),
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
);
CREATE TABLE IF NOT EXISTS work.plan_steps (
plan_step_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
kind TEXT NOT NULL,
description TEXT NOT NULL,
requires_capability_id TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (ordinal > 0),
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
CHECK (description <> '')
);
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
delegation_candidate_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_requirement_id TEXT NOT NULL,
kind TEXT NOT NULL,
executor_hint TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL,
reason TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_requirement_id <> ''),
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
CHECK (reason <> '')
);
RESET ROLE;
COMMIT;
@@ -2,7 +2,7 @@
param(
[string]$DockerContext = 'docker-prod',
[string]$SshHost = 'docker',
[string]$Endpoint = 'https://assistent.cin.su/healthz',
[string]$Endpoint = 'https://assistant.cin.su/healthz',
[string]$RemoteBackupRoot = '/home/m/assistant-backups/postgres',
[string]$NasBackupRoot = '\\nas\MST\codex\_backups\domovoy\postgres',
[string]$NasStatusPath = '\\nas\MST\codex\_monitoring\domovoy\status.json',
@@ -102,15 +102,15 @@ try {
catch { Add-Check 'tls-certificate' $false $_.Exception.Message }
try {
$assistantAddresses = @(Resolve-DnsName assistent.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
$assistantAddresses = @(Resolve-DnsName assistant.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
$dockerAddresses = @(Resolve-DnsName docker.cin.su -Server 8.8.8.8 -Type A -DnsOnly | Where-Object Type -eq A | Select-Object -ExpandProperty IPAddress | Sort-Object -Unique)
$same = $assistantAddresses.Count -gt 0 -and $dockerAddresses.Count -gt 0 -and @(Compare-Object $assistantAddresses $dockerAddresses).Count -eq 0
Add-Check 'public-dns' $same "assistent=$($assistantAddresses -join ',') docker=$($dockerAddresses -join ',')"
Add-Check 'public-dns' $same "assistant=$($assistantAddresses -join ',') docker=$($dockerAddresses -join ',')"
}
catch { Add-Check 'public-dns' $false $_.Exception.Message }
try {
$latestProduction = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n1") -join '').Trim()
$latestProduction = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1") -join '').Trim()
if ($LASTEXITCODE -ne 0 -or $latestProduction -notmatch '^\d{8}T\d{6}Z$') { throw 'invalid latest production backup' }
$productionTime = [DateTime]::ParseExact($latestProduction, "yyyyMMdd'T'HHmmss'Z'", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal)
$age = ([DateTime]::UtcNow - $productionTime).TotalHours
+3 -3
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000061-human-interaction-foundation.json",
"task_id": "DEV-000061",
"task_revision": 2
"task_file": "dev/tasks/DEV-000063-intent-work-planning-foundation.json",
"task_id": "DEV-000063",
"task_revision": 1
}
@@ -0,0 +1,140 @@
{
"task_id": "DEV-000062",
"task_revision": 1,
"architecture_revision": "ADR-0011",
"base_repo_revision": "69ba99099845ef24affc15bdd8f152717183bb10",
"risk_tier": "E",
"intent": "Add human intent, assertion/evidence boundary, project context and active focus semantics to Web Chat without new authority, provider binding, or execution capability.",
"requirement_ids": [
"ADR-0009",
"ADR-0010",
"ADR-0011",
"INV-EPI-001",
"INV-AUTH-001"
],
"allowed_change_scope": {
"modules": [
"interaction",
"situation",
"epistemics",
"work"
],
"paths": [
"architecture/adrs/ADR-0011-human-intent-project-context.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/interaction/**",
"contracts/situation/**",
"contracts/epistemics/**",
"modules/interaction/**",
"modules/situation/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"deploy/**",
"tests/human_interaction/**",
"tests/architecture/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000062-human-intent-evidence-project-context.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"modules/authority/**",
"modules/commit/**",
"cmd/commit-runtime/**",
"cmd/cognitive-worker/**",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 3,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0
},
"contracts": {
"consume": [
"CreatorSession",
"Interaction Conversation/HumanUtterance",
"Situation ProjectRegistry/ActiveFocus",
"Epistemics assertion boundary",
"Cognitive Gateway advisory"
],
"may_change": [
"contracts/interaction@v1",
"contracts/situation@v1",
"contracts/epistemics@v1"
]
},
"semantic_owner": "interaction+situation",
"canonical_state": [
"UtteranceInterpretation",
"ProjectReference",
"ConversationProjectFocus",
"SituationProjectRegistry",
"SituationActiveFocus"
],
"state_transitions": [
"utterance RECORDED->INTERPRETED",
"active focus SWITCHED by explicit human intent",
"correction appended without rewriting prior utterance"
],
"side_effects_and_privileges": [
"authenticated human semantic persistence",
"project focus reference update only",
"no execution",
"no verified fact creation",
"no ACTIVE binding promotion"
],
"persistence_and_migrations": [
"interaction owner-local interpretation/project reference tables",
"situation owner-local project registry and active focus tables",
"no cross-owner FK",
"no cross-owner SQL join dependency"
],
"failure_retry_restart": [
"cognition unavailable keeps persistence working",
"invalid candidate interpretation fails closed",
"ambiguous destructive request requires clarification",
"original human utterance and intent remain append-only"
],
"assumptions": [
"DEV-000061 is production baseline",
"existing CPU CANARY remains allowed only through current policy",
"ACTIVE bindings remain none",
"Project Registry V1 may be minimal and alias-based under Situation"
],
"acceptance_scenarios": [
"explicit project focus switch",
"assertion persists as unverified",
"directive persists without execution claim",
"correction/supersession append-only",
"inherited focus for safe non-destructive reference",
"multiple project references in one utterance",
"ambiguous destructive request asks clarification",
"cognition unavailable does not corrupt semantic state"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"architecture",
"owner ACL",
"LLM-free",
"human interaction",
"production live acceptance"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}
@@ -0,0 +1,151 @@
{
"task_id": "DEV-000063",
"task_revision": 1,
"architecture_revision": "ADR-0012",
"base_repo_revision": "18750c2ae69c90336900b333821cfbcf68769c16",
"risk_tier": "E",
"intent": "Add general HelpPath and optional Intent -> Goal -> Work Planning foundation without making Domovoy a Codex prompt generator or granting execution authority.",
"requirement_ids": [
"ADR-0012",
"INV-AUTH-001",
"INV-EPI-001",
"INV-OWN-001"
],
"allowed_change_scope": {
"modules": [
"work",
"interaction",
"situation",
"epistemics",
"capability"
],
"paths": [
"architecture/adrs/ADR-0012-intent-work-planning-foundation.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/work/**",
"contracts/interaction/**",
"contracts/capability/**",
"contracts/epistemics/**",
"modules/work/**",
"modules/interaction/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"tests/work_planning/**",
"tests/architecture/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000063-intent-work-planning-foundation.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"modules/authority/**",
"modules/commit/**",
"cmd/commit-runtime/**",
"cmd/cognitive-worker/**",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 3,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0
},
"contracts": {
"consume": [
"Interaction OriginalHumanIntent",
"Interaction UtteranceInterpretation",
"Situation ActiveProjectFocus",
"Epistemics VerificationState",
"Capability IDs/health/registry concepts",
"Authority boundary concepts"
],
"may_change": [
"contracts/work@v1",
"contracts/interaction@v1",
"contracts/capability@v1"
]
},
"semantic_owner": "work",
"canonical_state": [
"Goal",
"HelpPath",
"GoalLifecycle",
"PlanCandidate",
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
],
"state_transitions": [
"help path ANSWER/COGNITION/DIRECT_CAPABILITY/DURABLE_WORK/CAPABILITY_GAP selected before durable planning",
"goal PROPOSED->UNDERSTOOD->PLANNING->READY when durable work is actually needed",
"plan candidate PROPOSED->VALIDATED->ACCEPTED",
"no transition to IN_PROGRESS without future execution authority"
],
"side_effects_and_privileges": [
"planning-only canonical Work persistence",
"ordinary answer/direct capability/capability gap paths without durable Work persistence",
"no external mutation",
"no Commit/Executor invocation",
"no ACTIVE binding promotion",
"cognitive routing only through existing gateway when later enabled"
],
"persistence_and_migrations": [
"work owner-local goals",
"work owner-local plan candidates",
"work owner-local plan steps",
"work owner-local capability requirements",
"work owner-local delegation candidates",
"no cross-owner FK",
"no cross-owner SQL join dependency"
],
"failure_retry_restart": [
"intent persists even if planning falls back deterministically",
"Goal/Work/Plan are optional and not created for simple help",
"invalid plan candidate fails closed",
"cognition unavailable keeps deterministic planning available",
"planning never claims execution/completion"
],
"assumptions": [
"DEV-000062 is production baseline",
"Work owner is canonical Goal/Task/Decision owner per Core Semantic Model",
"existing CPU CANARY remains not ACTIVE",
"docker-test prohibited"
],
"acceptance_scenarios": [
"investigate UPO performance creates investigation plan, not Codex prompt",
"add support X in Assistant creates MODIFY_CODE requirement without hardcoded executor",
"review new email creates READ_EMAIL and COGNITIVE_REASONING requirements without Codex",
"reminder creates SCHEDULE_REMINDER requirement",
"unverified disk assertion creates verification requirement before risky model install",
"send email creates SEND_EMAIL planning without execution or authority claim",
"cognition unavailable still persists intent/goal/plan when durable work is needed",
"simple explanation can use answer/cognition path without durable Goal/Work/Plan",
"capability gap remains executor-neutral and future self-extension-ready"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"architecture",
"owner ACL",
"LLM-free",
"work planning",
"production live acceptance"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}
+24 -6
View File
@@ -1,16 +1,34 @@
# Assistant project status
Audit date: 2026-08-17. Active implementation: DEV-000061 revision 2.
Audit date: 2026-08-17. Active implementation: DEV-000063 revision 1.
## DEV-000061 checkpoint
## DEV-000063 checkpoint
- `DEV_000061_STATUS = IN_PROGRESS`
- `LAST_COMPLETED = DEV-000061 packet rev.2, ADR-0009, ADR-0010, Interaction registry declaration, CredentialRef file resolver foundation, interaction schema migration/bootstrap, interaction repository, Core Runtime chat persistence endpoint, Creator Web /chat surface and LOCAL_ONLY Cognitive Gateway handoff`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -task dev/tasks/DEV-000061-human-interaction-foundation.json; go run ./cmd/devctl check-architecture`
- `DEV_000063_STATUS = COMPLETED`
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests, migration 017, production deployment and live /chat acceptance`
- `HELP_PATH_OPTIONALITY = enabled; simple answer/cognition/direct capability/capability gap can bypass durable Goal/Work/Plan`
- `GOAL_WORK_PLAN_OPTIONALITY = enabled; durable records are created only for complex work/planning cases`
- `PROJECT_NEUTRALITY = project context is optional opaque Work reference; Project is not mandatory context`
- `CODEX_NEUTRALITY = no Codex-specific canonical entity or executor selection`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live /chat acceptance`
- `LIVE_ACCEPTANCE = PASS: simple help uses cognition without Work; reminder uses direct scheduling-capability path without Work; complex УПО investigation creates one Work Goal/Plan; capability gap remains executor-neutral without Work`
- `PRODUCTION_REVISION = 9d50a3ec635782b59ab3335808d6d1e026be48a4`
- `PRODUCTION_HEALTH = healthy`
- `NEXT_STEP = DEV-000064 capability resolution/acquisition or concrete scheduler/mail/codex adapter foundation, if separately approved`
- `REAL_BLOCKERS = none known`
## DEV-000062 checkpoint
- `DEV_000062_STATUS = COMPLETED`
- `LAST_COMPLETED = DEV-000062 packet rev.1, ADR-0011, Interaction/Situation/Epistemics contract extensions, Situation Project Registry V1, active project focus semantics, Interaction utterance interpretation/project references, append-only correction/supersession foundation, Core Runtime semantic persistence wiring, Creator Web /chat focus display, production deployment and live /chat acceptance`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; production live /chat acceptance`
- `ACTIVE_BINDINGS = none`
- `CANARY_BINDING = unchanged; not promoted to ACTIVE`
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
- `NEXT_STEP = production interaction_role delivery: copy protected local interaction_role credential into the existing assistant_core_secrets backend as /run/secrets/interaction_db_password, set/validate PostgreSQL interaction_role password, apply migration, rebuild/deploy affected production services, then run live /chat acceptance`
- `PRODUCTION_REVISION = 445e9edd7c5b614b58dd925f89aa9fe49cf0a1d6`
- `HUMAN_CHAT_URL = https://assistant.cin.su/chat`
- `LIVE_ACCEPTANCE = PASS: explicit УПО focus, unverified assertion, directive/no execution, correction, explicit Assistant focus, current-focus question answered without cognition`
- `NEXT_STEP = DEV-000063 Project Copilot planning, if separately approved`
- `REAL_BLOCKERS = none known`
## Current milestone
+5 -5
View File
@@ -99,13 +99,13 @@ func NewHumanUtterance(conversation interaction.Conversation, request interactio
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
lower := strings.ToLower(strings.TrimSpace(message))
switch {
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так"):
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
return interaction.SpeechActCorrection
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(lower, "объясни"):
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
return interaction.SpeechActRequestToInvestigate
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой"):
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить") || strings.Contains(lower, "напомни") || strings.Contains(lower, "подключи"):
return interaction.SpeechActDirective
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
return interaction.SpeechActDecision
@@ -113,9 +113,9 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
return interaction.SpeechActPreference
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
return interaction.SpeechActHypothesis
case strings.Contains(lower, "да") || strings.Contains(lower, "подтверж"):
case lower == "да" || strings.HasPrefix(lower, "да,") || strings.Contains(lower, "подтверж"):
return interaction.SpeechActConfirmation
case strings.Contains(lower, "нет") || strings.Contains(lower, "отмен"):
case lower == "нет" || strings.HasPrefix(lower, "нет,") || strings.Contains(lower, "отмен"):
return interaction.SpeechActRejection
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
return interaction.SpeechActConversational
+13
View File
@@ -36,6 +36,19 @@ func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) {
}
}
func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t *testing.T) {
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
for _, message := range []string{"Напомни завтра позвонить врачу.", "Подключи интеграцию с неизвестным сервисом."} {
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(10, 0))
if err != nil {
t.Fatal(err)
}
if utterance.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for %q, got %s", message, utterance.SpeechAct)
}
}
}
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
if err == nil {
+242
View File
@@ -0,0 +1,242 @@
package domain
import (
"strings"
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
"git.cin.su/m/Assistant/platform/ids"
)
type SemanticResult struct {
Interpretation interaction.UtteranceInterpretation
Projects []interaction.ProjectReference
Relations []interaction.InteractionRelation
ActiveFocus *situation.ActiveProjectFocus
Reply string
NeedsCognition bool
}
func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) {
act := ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
interpretationID, err := ids.New()
if err != nil {
return SemanticResult{}, err
}
millis := now.UnixMilli()
result := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
ID: interaction.InterpretationID(interpretationID),
UtteranceID: utterance.ID,
SpeechAct: act,
ConfidenceMillis: confidenceFor(act),
Source: "deterministic-v1",
AssertionVerification: string(epistemics.VerificationUnverified),
RequestedOutcome: requestedOutcome(utterance.Content, act),
Clarification: interaction.ClarificationNotRequired,
RecordedAtUnixMillis: millis,
},
NeedsCognition: NeedsCognition(act),
}
explicitFocus := explicitProjectFocus(utterance.Content, projects)
projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)
if err != nil {
return SemanticResult{}, err
}
result.Projects = projectRefs
if explicitFocus != nil {
result.ActiveFocus = &situation.ActiveProjectFocus{
ActorID: utterance.ActorID,
PrincipalID: utterance.PrincipalID,
ProjectID: explicitFocus.ProjectID,
ProjectDisplayName: explicitFocus.ProjectDisplayName,
Provenance: situation.ProjectResolutionExplicit,
ConfidenceMillis: explicitFocus.ConfidenceMillis,
SourceUtteranceID: string(utterance.ID),
UpdatedAtUnixMillis: millis,
}
}
if isAmbiguousDestructive(act, utterance.Content, result.Projects) {
result.Interpretation.Clarification = interaction.ClarificationRequired
result.Reply = "Уточните проект и цель: это потенциально разрушительное изменение, а текущая ссылка недостаточно надёжна."
return result, nil
}
if act == interaction.SpeechActCorrection {
relations, err := correctionRelations(utterance, millis)
if err != nil {
return SemanticResult{}, err
}
result.Relations = append(result.Relations, relations...)
}
result.Reply = semanticReply(utterance.Content, result, previousFocus)
if isCurrentFocusQuestion(utterance.Content, act) && previousFocus != nil {
result.NeedsCognition = false
}
return result, nil
}
func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference {
lower := strings.ToLower(message)
if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) {
return nil
}
for _, project := range projects {
for _, alias := range projectAliases(project) {
if strings.Contains(lower, alias) {
return &interaction.ProjectReference{ProjectID: project.ID, ProjectDisplayName: project.DisplayName, Provenance: situation.ProjectResolutionExplicit, ConfidenceMillis: 980}
}
}
}
return nil
}
func projectReferences(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, explicit *interaction.ProjectReference, previousFocus *situation.ActiveProjectFocus, millis int64) ([]interaction.ProjectReference, error) {
if explicit != nil {
ref, err := newProjectReference(utterance.ID, explicit.ProjectID, explicit.ProjectDisplayName, interaction.ProjectRolePrimary, explicit.Provenance, explicit.ConfidenceMillis, millis)
return []interaction.ProjectReference{ref}, err
}
lower := strings.ToLower(utterance.Content)
var refs []interaction.ProjectReference
for _, project := range projects {
for _, alias := range projectAliases(project) {
if strings.Contains(lower, alias) {
role := projectRoleForAlias(lower, alias)
ref, err := newProjectReference(utterance.ID, project.ID, project.DisplayName, role, situation.ProjectResolutionEntityReference, 850, millis)
if err != nil {
return nil, err
}
refs = append(refs, ref)
break
}
}
}
if len(refs) == 0 && previousFocus != nil && strings.Contains(lower, "туда") {
ref, err := newProjectReference(utterance.ID, previousFocus.ProjectID, previousFocus.ProjectDisplayName, interaction.ProjectRolePrimary, situation.ProjectResolutionInheritedFocus, 700, millis)
if err != nil {
return nil, err
}
refs = append(refs, ref)
}
if len(refs) > 1 {
hasPrimary := false
for _, ref := range refs {
hasPrimary = hasPrimary || ref.Role == interaction.ProjectRolePrimary
}
if !hasPrimary {
refs[len(refs)-1].Role = interaction.ProjectRolePrimary
}
}
return refs, nil
}
func projectRoleForAlias(lower string, alias string) interaction.ProjectReferenceRole {
aliasIndex := strings.Index(lower, alias)
if aliasIndex < 0 {
return interaction.ProjectRoleRelated
}
before := strings.TrimSpace(lower[:aliasIndex])
afterStart := aliasIndex + len(alias)
after := ""
if afterStart < len(lower) {
after = strings.TrimSpace(lower[afterStart:])
}
if strings.Contains(before, "как") || strings.Contains(before, "посмотри") || strings.Contains(after, "как источник") {
return interaction.ProjectRoleReferenceSource
}
targetMarkers := []string{"сделай", "добавь", "измени", "поменяй", "перенеси", "используй", "настрой", "в "}
for _, marker := range targetMarkers {
if strings.Contains(before, marker) {
return interaction.ProjectRolePrimary
}
}
return interaction.ProjectRoleRelated
}
func newProjectReference(utteranceID interaction.HumanUtteranceID, projectID situation.ProjectID, display string, role interaction.ProjectReferenceRole, provenance situation.ProjectResolutionProvenance, confidence int64, millis int64) (interaction.ProjectReference, error) {
id, err := ids.New()
if err != nil {
return interaction.ProjectReference{}, err
}
return interaction.ProjectReference{ID: interaction.ProjectReferenceID(id), UtteranceID: utteranceID, ProjectID: projectID, ProjectDisplayName: display, Role: role, Provenance: provenance, ConfidenceMillis: confidence, RecordedAtUnixMillis: millis}, nil
}
func projectAliases(project situation.ProjectDescriptor) []string {
values := []string{strings.ToLower(project.DisplayName), strings.ToLower(string(project.ID))}
for _, alias := range project.Aliases {
values = append(values, strings.ToLower(string(alias)))
}
return values
}
func correctionRelations(utterance interaction.HumanUtterance, millis int64) ([]interaction.InteractionRelation, error) {
if utterance.CorrectsUtteranceID == "" {
return nil, nil
}
id, err := ids.New()
if err != nil {
return nil, err
}
return []interaction.InteractionRelation{{ID: interaction.InteractionRelationID(id), FromUtteranceID: utterance.ID, ToUtteranceID: utterance.CorrectsUtteranceID, Kind: interaction.RelationSupersedes, RecordedAtUnixMillis: millis}}, nil
}
func confidenceFor(act interaction.SpeechAct) int64 {
if act == interaction.SpeechActUnknown {
return 300
}
return 800
}
func requestedOutcome(message string, act interaction.SpeechAct) string {
if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate {
return strings.TrimSpace(message)
}
return ""
}
func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool {
return strings.Contains(strings.ToLower(message), "туда") && act != interaction.SpeechActUnknown
}
func isAmbiguousDestructive(act interaction.SpeechAct, message string, projects []interaction.ProjectReference) bool {
lower := strings.ToLower(message)
destructive := strings.Contains(lower, "удали") || strings.Contains(lower, "снеси") || strings.Contains(lower, "удалить")
if !destructive || act != interaction.SpeechActDirective {
return false
}
if len(projects) == 0 {
return true
}
return projects[0].Provenance == situation.ProjectResolutionInheritedFocus || projects[0].Provenance == situation.ProjectResolutionInferred
}
func semanticReply(message string, result SemanticResult, previousFocus *situation.ActiveProjectFocus) string {
if result.ActiveFocus != nil {
return "Фокус переключён: " + result.ActiveFocus.ProjectDisplayName + "."
}
switch result.Interpretation.SpeechAct {
case interaction.SpeechActAssertion:
return "Записал как утверждение человека. Это пока не проверенный факт."
case interaction.SpeechActDirective:
return "Записал просьбу как намерение пользователя. Ничего не выполнено без отдельного подтверждённого действия."
case interaction.SpeechActDecision:
return "Записал это как явное решение пользователя для дальнейшей обработки."
case interaction.SpeechActCorrection:
return "Исправление записано новым сообщением; прежняя история сохранена."
case interaction.SpeechActQuestion:
lower := strings.ToLower(message)
if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) {
return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "."
}
}
return FallbackText(result.Interpretation.SpeechAct)
}
func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool {
if act != interaction.SpeechActQuestion {
return false
}
lower := strings.ToLower(message)
return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")
}
@@ -0,0 +1,132 @@
package domain
import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
)
func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) {
result := interpretForTest(t, "Сейчас работаем с УПО.", nil, "")
if result.ActiveFocus == nil || result.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit UPO focus, got %#v", result.ActiveFocus)
}
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
}
if result.Projects[0].Provenance != situation.ProjectResolutionExplicit {
t.Fatalf("expected explicit provenance, got %#v", result.Projects)
}
}
func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) {
result := interpretForTest(t, "На сервере сейчас свободно 300 ГБ.", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.AssertionVerification != "UNVERIFIED" || !strings.Contains(result.Reply, "не проверенный факт") {
t.Fatalf("expected unverified human assertion, got %#v reply=%q", result.Interpretation, result.Reply)
}
}
func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") {
t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply)
}
}
func TestInterpretUtteranceCorrectionIsAppendOnlySupersedes(t *testing.T) {
result := interpretForTest(t, "Нет, используй УПО2.", activeFocus("upo", "УПО"), "utterance-previous")
if result.Interpretation.SpeechAct != interaction.SpeechActCorrection {
t.Fatalf("expected correction, got %s", result.Interpretation.SpeechAct)
}
if len(result.Relations) != 1 || result.Relations[0].Kind != interaction.RelationSupersedes || result.Relations[0].ToUtteranceID != "utterance-previous" {
t.Fatalf("expected supersedes relation, got %#v", result.Relations)
}
}
func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
result := interpretForTest(t, "Что мы сейчас делаем?", activeFocus("assistant", "Assistant"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %s", result.Interpretation.SpeechAct)
}
if !strings.Contains(result.Reply, "Assistant") {
t.Fatalf("expected focus-aware reply, got %q", result.Reply)
}
if result.NeedsCognition {
t.Fatalf("focus question should be answered from canonical focus without cognition")
}
}
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "")
if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
t.Fatalf("expected inherited Assistant focus, got %#v", result.Projects)
}
}
func TestInterpretUtteranceMultiProjectReferenceRoles(t *testing.T) {
result := interpretForTest(t, "Посмотри, как это сделано в Assistant, и сделай аналогично в УПО.", nil, "")
if len(result.Projects) != 2 {
t.Fatalf("expected two project references, got %#v", result.Projects)
}
roles := map[situation.ProjectID]interaction.ProjectReferenceRole{}
for _, ref := range result.Projects {
roles[ref.ProjectID] = ref.Role
}
if roles["assistant"] != interaction.ProjectRoleReferenceSource || roles["upo"] != interaction.ProjectRolePrimary {
t.Fatalf("unexpected roles: %#v", roles)
}
}
func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.T) {
result := interpretForTest(t, "Удали туда старую базу.", activeFocus("assistant", "Assistant"), "")
if result.Interpretation.Clarification != interaction.ClarificationRequired {
t.Fatalf("expected clarification, got %#v", result.Interpretation)
}
if result.NeedsCognition {
t.Fatalf("ambiguous destructive directive must not require cognition")
}
}
func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult {
t.Helper()
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message, CorrectsUtteranceID: corrects}, time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
result, err := InterpretUtterance(utterance, projectRegistryForTest(), previous, time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
return result
}
func projectRegistryForTest() []situation.ProjectDescriptor {
return []situation.ProjectDescriptor{
{ID: "assistant", DisplayName: "Assistant", Aliases: []situation.ProjectAlias{"assistant", "ассистент", "домовой"}},
{ID: "upo", DisplayName: "УПО", Aliases: []situation.ProjectAlias{"упо", "upo"}},
}
}
func activeFocus(projectID situation.ProjectID, displayName string) *situation.ActiveProjectFocus {
return &situation.ActiveProjectFocus{
ActorID: identity.ActorID("actor-1"),
PrincipalID: identity.PrincipalID("principal-1"),
ProjectID: projectID,
ProjectDisplayName: displayName,
Provenance: situation.ProjectResolutionExplicit,
ConfidenceMillis: 980,
SourceUtteranceID: "utterance-focus",
UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(),
}
}
+257
View File
@@ -0,0 +1,257 @@
package domain
import (
"errors"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidPlanningInput = errors.New("invalid planning input")
var ErrInvalidPlanCandidate = errors.New("invalid plan candidate")
func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.PlanningResult, error) {
if input.UtteranceID == "" || input.IntentID == "" || input.ActorID == "" || input.PrincipalID == "" || strings.TrimSpace(input.Message) == "" {
return work.PlanningResult{}, ErrInvalidPlanningInput
}
millis := now.UnixMilli()
goalID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
candidateID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
planID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
goal := work.Goal{
ID: work.GoalID(goalID),
SourceIntentID: input.IntentID,
SourceUtteranceID: input.UtteranceID,
ActorID: input.ActorID,
PrincipalID: input.PrincipalID,
ScopeID: input.ScopeID,
ProjectID: input.ProjectID,
ProjectDisplayName: input.ProjectDisplayName,
DesiredOutcome: desiredOutcome(input),
Constraints: "planning-only; no execution authority; no external mutation",
State: work.GoalReady,
Provenance: input.ProjectProvenance,
Revision: 1,
CreatedAtUnixMillis: millis,
UpdatedAtUnixMillis: millis,
}
candidate := work.PlanCandidate{
ID: work.PlanCandidateID(candidateID),
GoalID: goal.ID,
Source: "deterministic-planner-v1",
State: work.PlanCandidateAccepted,
Summary: planSummary(input),
ValidationSummary: "validated: structured deterministic candidate; executor-neutral; authority not granted",
RequiresClarification: false,
AcceptedAtUnixMillis: millis,
RecordedAtUnixMillis: millis,
}
addScenarioPlan(input, millis, &candidate)
if hasRiskyUnverifiedAssertion(input) {
addRequirement(&candidate, capability.CapabilityKindVerifyFact, "verify the user-provided resource assertion before risky work", epistemics.VerificationUnverified, millis)
addStep(&candidate, work.PlanStepVerify, "Verify the reported server free-space/capacity using trusted observations before any large model installation.", len(candidate.Steps)+1, millis)
}
if err := ValidatePlanCandidate(candidate); err != nil {
return work.PlanningResult{}, err
}
plan := work.WorkPlan{ID: work.WorkPlanID(planID), GoalID: goal.ID, AcceptedCandidateID: candidate.ID, Revision: 1, State: candidate.State, Summary: candidate.Summary, RecordedAtUnixMillis: millis}
return work.PlanningResult{Goal: goal, Candidate: candidate, Plan: plan, Reply: planningReply(goal, candidate)}, nil
}
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case containsAny(lower, "напомни", "напомин"):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case requiresDurableWork(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
default:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
}
}
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
return ErrInvalidPlanCandidate
}
switch candidate.State {
case work.PlanCandidateProposed, work.PlanCandidateValidated, work.PlanCandidateAccepted, work.PlanCandidateRejected:
default:
return ErrInvalidPlanCandidate
}
ordinals := map[int]bool{}
for _, step := range candidate.Steps {
if step.ID == "" || step.PlanCandidateID != candidate.ID || step.Ordinal < 1 || step.Kind == "" || step.Description == "" || ordinals[step.Ordinal] {
return ErrInvalidPlanCandidate
}
ordinals[step.Ordinal] = true
}
seenRequirements := map[work.CapabilityRequirementID]bool{}
for _, requirement := range candidate.Requirements {
if requirement.ID == "" || requirement.PlanCandidateID != candidate.ID || requirement.CapabilityKind == "" || requirement.Purpose == "" || requirement.State == "" || requirement.EpistemicPrecondition == "" {
return ErrInvalidPlanCandidate
}
seenRequirements[requirement.ID] = true
}
for _, delegation := range candidate.Delegations {
if delegation.ID == "" || delegation.PlanCandidateID != candidate.ID || delegation.CapabilityRequirementID == "" || delegation.Kind == "" || delegation.State == "" || !seenRequirements[delegation.CapabilityRequirementID] {
return ErrInvalidPlanCandidate
}
}
return nil
}
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
case containsAny(lower, "письм", "почт"):
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
if containsAny(lower, "ответ", "ответить", "скажи") {
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
}
if containsAny(lower, "отправь", "напиши") {
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
}
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
case containsAny(lower, "напомни", "завтра", "напомин"):
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
case containsAny(lower, "отправь", "ивану", "сообщи"):
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
default:
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
candidate.RequiresClarification = true
}
for _, requirement := range candidate.Requirements {
addDelegation(candidate, requirement, millis)
}
}
func requiresDurableWork(lower string) bool {
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
}
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Requirements = append(candidate.Requirements, work.CapabilityRequirement{ID: work.CapabilityRequirementID(id), PlanCandidateID: candidate.ID, CapabilityKind: kind, Purpose: purpose, State: work.CapabilityRequirementMissing, EpistemicPrecondition: precondition, RecordedAtUnixMillis: millis})
}
func addStep(candidate *work.PlanCandidate, kind work.PlanStepKind, description string, ordinal int, millis int64) {
id, err := ids.New()
if err != nil {
return
}
var requirementID work.CapabilityRequirementID
if len(candidate.Requirements) > 0 {
requirementID = candidate.Requirements[len(candidate.Requirements)-1].ID
}
candidate.Steps = append(candidate.Steps, work.PlanStep{ID: work.PlanStepID(id), PlanCandidateID: candidate.ID, Ordinal: ordinal, Kind: kind, Description: description, RequiresCapabilityID: requirementID, RecordedAtUnixMillis: millis})
}
func addDelegation(candidate *work.PlanCandidate, requirement work.CapabilityRequirement, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Delegations = append(candidate.Delegations, work.DelegationCandidate{ID: work.DelegationCandidateID(id), PlanCandidateID: candidate.ID, CapabilityRequirementID: requirement.ID, Kind: delegationKind(requirement.CapabilityKind), ExecutorHint: "", State: work.DelegationCandidateBlocked, Reason: "no concrete executor selected in DEV-000063 planning-only mode", RecordedAtUnixMillis: millis})
}
func delegationKind(kind capability.CapabilityKind) work.DelegationKind {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindWebResearch:
return work.DelegationKindAgent
case capability.CapabilityKindReadEmail, capability.CapabilityKindSendEmail, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindScheduleReminder, capability.CapabilityKindInspectProject, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact:
return work.DelegationKindAdapter
default:
return work.DelegationKindUnavailable
}
}
func desiredOutcome(input work.PlanningInput) string {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "производительн"):
return "identify likely performance cause with sufficient evidence"
case containsAny(lower, "письм", "почт"):
return "review communications and identify what needs response"
case containsAny(lower, "напомни", "напомин"):
return "create a future reminder when scheduling capability is available"
case containsAny(lower, "отправь", "сообщи"):
return "communicate requested information to the recipient after authority"
case containsAny(lower, "добавь", "поддержк", "возможность"):
return "software project gains the requested capability after validated implementation"
default:
return strings.TrimSpace(input.Message)
}
}
func planSummary(input work.PlanningInput) string {
return "Planning foundation for: " + desiredOutcome(input)
}
func planningReply(goal work.Goal, candidate work.PlanCandidate) string {
requirements := make([]string, 0, len(candidate.Requirements))
for _, requirement := range candidate.Requirements {
requirements = append(requirements, string(requirement.CapabilityKind))
}
project := goal.ProjectDisplayName
if project == "" {
project = "текущий контекст"
}
return "Понял цель: " + goal.DesiredOutcome + ". Контекст: " + project + ". План сохранён в безопасном режиме: нужны capability " + strings.Join(requirements, ", ") + ". Исполнение этих инструментов пока не выполнено и требует отдельной capability/authority."
}
func hasRiskyUnverifiedAssertion(input work.PlanningInput) bool {
lower := strings.ToLower(input.Message)
return input.AssertionVerification == epistemics.VerificationUnverified && containsAny(lower, "300 гб", "свободно", "большую модель", "установи", "модель")
}
func containsAny(value string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(value, needle) {
return true
}
}
return false
}
+161
View File
@@ -0,0 +1,161 @@
package domain
import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
)
func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) {
result := planForTest(t, "Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindCognitiveReasoning)
for _, delegation := range result.Candidate.Delegations {
if strings.Contains(strings.ToLower(delegation.ExecutorHint), "codex") {
t.Fatalf("planning must not hardcode Codex executor: %#v", delegation)
}
}
if strings.Contains(strings.ToLower(result.Candidate.Summary), "prompt") {
t.Fatalf("canonical plan must not be prompt-centric: %q", result.Candidate.Summary)
}
}
func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) {
result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests)
for _, delegation := range result.Candidate.Delegations {
if delegation.ExecutorHint != "" {
t.Fatalf("executor must remain unselected in canonical planning: %#v", delegation)
}
}
}
func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) {
result := planForTest(t, "Посмотри новые письма и скажи, на какие нужно ответить.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindReadEmail, capability.CapabilityKindCognitiveReasoning)
requireNoKind(t, result, capability.CapabilityKindModifyCode)
}
func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) {
result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindScheduleReminder)
requireNoKind(t, result, capability.CapabilityKindModifyCode)
}
func TestPlanningRiskyUnverifiedAssertionRequiresVerification(t *testing.T) {
result := planForTest(t, "На сервере 300 ГБ свободно, установи большую модель.", "ASSERTION", epistemics.VerificationUnverified)
requireKinds(t, result, capability.CapabilityKindVerifyFact)
found := false
for _, requirement := range result.Candidate.Requirements {
if requirement.CapabilityKind == capability.CapabilityKindVerifyFact && requirement.EpistemicPrecondition == epistemics.VerificationUnverified {
found = true
}
}
if !found {
t.Fatalf("expected unverified verification precondition: %#v", result.Candidate.Requirements)
}
}
func TestPlanningSendEmailPreservesAuthorityBoundary(t *testing.T) {
result := planForTest(t, "Отправь Ивану письмо.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindSendEmail)
if result.Goal.State == work.GoalCompleted || !strings.Contains(result.Reply, "требует отдельной capability/authority") {
t.Fatalf("planning must not claim execution/completion: goal=%#v reply=%q", result.Goal, result.Reply)
}
}
func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) {
result := planForTest(t, "Сделай что-нибудь полезное.", "DIRECTIVE", epistemics.VerificationUnknown)
if result.Goal.ID == "" || result.Candidate.ID == "" || result.Plan.ID == "" {
t.Fatalf("expected persisted planning identities: %#v", result)
}
if result.Candidate.Source != "deterministic-planner-v1" {
t.Fatalf("expected deterministic fallback source: %s", result.Candidate.Source)
}
}
func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan {
t.Fatalf("simple help must not require durable planning: %#v", path)
}
}
func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder {
t.Fatalf("reminder should be direct scheduling capability path: %#v", path)
}
}
func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableGoal || !path.NeedsDurableWork || !path.NeedsDurablePlan {
t.Fatalf("complex investigation should need durable planning: %#v", path)
}
}
func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
t.Fatalf("expected capability gap path: %#v", path)
}
if strings.Contains(strings.ToLower(path.Reply), "codex") {
t.Fatalf("capability gap must not be represented as Codex task: %#v", path)
}
}
func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult {
t.Helper()
result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
if err := ValidatePlanCandidate(result.Candidate); err != nil {
t.Fatal(err)
}
return result
}
func planningInputForTest(message string, act string, verification epistemics.VerificationState) work.PlanningInput {
return work.PlanningInput{
ConversationID: "conversation-1",
UtteranceID: "utterance-1",
IntentID: "intent-1",
ActorID: identity.ActorID("actor-1"),
PrincipalID: identity.PrincipalID("principal-1"),
ProjectID: work.ProjectRef("upo"),
ProjectDisplayName: "УПО",
ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"),
Message: message,
SpeechAct: work.SpeechActRef(act),
AssertionVerification: verification,
ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
}
}
func requireKinds(t *testing.T, result work.PlanningResult, kinds ...capability.CapabilityKind) {
t.Helper()
actual := map[capability.CapabilityKind]bool{}
for _, requirement := range result.Candidate.Requirements {
actual[requirement.CapabilityKind] = true
}
for _, kind := range kinds {
if !actual[kind] {
t.Fatalf("missing capability kind %s in %#v", kind, result.Candidate.Requirements)
}
}
}
func requireNoKind(t *testing.T, result work.PlanningResult, kind capability.CapabilityKind) {
t.Helper()
for _, requirement := range result.Candidate.Requirements {
if requirement.CapabilityKind == kind {
t.Fatalf("unexpected capability kind %s in %#v", kind, result.Candidate.Requirements)
}
}
}