@lamentis/naome 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -2
- package/README.md +5 -0
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/context/select.rs +58 -4
- package/crates/naome-core/src/intent/classifier.rs +56 -81
- package/crates/naome-core/src/intent/envelope.rs +173 -19
- package/crates/naome-core/src/intent/legacy_response.rs +2 -0
- package/crates/naome-core/src/intent/model.rs +6 -0
- package/crates/naome-core/src/intent/resolver.rs +25 -0
- package/crates/naome-core/src/intent/risk.rs +11 -1
- package/crates/naome-core/src/intent.rs +1 -1
- package/crates/naome-core/src/route/context.rs +8 -0
- package/crates/naome-core/tests/context.rs +92 -0
- package/crates/naome-core/tests/intent.rs +98 -18
- package/crates/naome-core/tests/intent_support/mod.rs +39 -1
- package/crates/naome-core/tests/intent_v2.rs +299 -10
- package/crates/naome-core/tests/repo_support/routes.rs +8 -2
- package/crates/naome-core/tests/route_baseline.rs +29 -0
- package/crates/naome-core/tests/route_completion.rs +26 -5
- package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
- package/crates/naome-core/tests/route_user_diff.rs +1 -1
- package/crates/naome-core/tests/task_state_compact.rs +7 -1
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/manifest.json +2 -2
- package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
- package/templates/naome-root/docs/naome/architecture.md +9 -0
- package/crates/naome-core/src/intent/patterns.rs +0 -170
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.3.
|
|
79
|
+
version = "1.3.8"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.3.
|
|
87
|
+
version = "1.3.8"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
package/README.md
CHANGED
|
@@ -41,6 +41,11 @@ For agent-driven work, route the user's request through the harness:
|
|
|
41
41
|
naome route --prompt-file /path/to/prompt.txt --execute --json
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Prompt files should start with a fenced `naome-prompt-envelope-v1` JSON
|
|
45
|
+
envelope that normalizes the raw user text into canonical routing fields. A raw
|
|
46
|
+
natural-language prompt routes to a non-mutating normalization decision instead
|
|
47
|
+
of becoming a task by keyword inference.
|
|
48
|
+
|
|
44
49
|
## Why NAOME?
|
|
45
50
|
|
|
46
51
|
- Keeps agents inside explicit task scope.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
use std::path::Path;
|
|
1
|
+
use std::path::{Component, Path};
|
|
2
2
|
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
|
|
5
|
+
use crate::intent::prompt_envelope_json;
|
|
3
6
|
use crate::{git, models::NaomeError};
|
|
4
7
|
|
|
5
8
|
use super::helpers::{capsule_for_paths, context_item, is_source_path, normalize_paths};
|
|
@@ -7,6 +10,13 @@ use super::types::{ContextBudgetLedger, ContextItem, ContextSelection};
|
|
|
7
10
|
|
|
8
11
|
const MAX_CONTEXT_FILES: usize = 12;
|
|
9
12
|
|
|
13
|
+
#[derive(Debug, Deserialize)]
|
|
14
|
+
#[serde(rename_all = "camelCase")]
|
|
15
|
+
struct PromptEnvelopeContext {
|
|
16
|
+
schema: Option<String>,
|
|
17
|
+
referenced_paths: Option<Vec<String>>,
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
pub fn select_context_for_changed_paths(root: &Path) -> Result<ContextSelection, NaomeError> {
|
|
11
21
|
let paths = git::changed_paths(root)?;
|
|
12
22
|
Ok(selection_for_paths(root, "changed", paths))
|
|
@@ -19,7 +29,9 @@ pub fn select_context_for_prompt(
|
|
|
19
29
|
Ok(selection_for_paths(
|
|
20
30
|
root,
|
|
21
31
|
"prompt",
|
|
22
|
-
|
|
32
|
+
envelope_referenced_paths(prompt.as_ref())
|
|
33
|
+
.filter(|paths| !paths.is_empty())
|
|
34
|
+
.unwrap_or_else(|| mentioned_paths(root, prompt.as_ref())),
|
|
23
35
|
))
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -119,7 +131,7 @@ fn forbidden_context() -> Vec<String> {
|
|
|
119
131
|
.collect()
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
134
|
+
fn mentioned_paths(root: &Path, prompt: &str) -> Vec<String> {
|
|
123
135
|
prompt
|
|
124
136
|
.split_whitespace()
|
|
125
137
|
.map(|token| {
|
|
@@ -127,8 +139,50 @@ fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
|
127
139
|
matches!(ch, '"' | '\'' | '`' | ',' | ';' | ':' | ')' | '(')
|
|
128
140
|
})
|
|
129
141
|
})
|
|
130
|
-
.filter(|token| token.contains('/') || is_source_path(token) || token.ends_with(".md"))
|
|
131
142
|
.filter(|token| !token.starts_with('-') && !token.starts_with("http"))
|
|
132
143
|
.map(|token| token.trim_start_matches("./").replace('\\', "/"))
|
|
144
|
+
.filter(|path| path_looks_contextual(path))
|
|
145
|
+
.filter(|path| path_exists_or_has_repo_parent(root, path))
|
|
133
146
|
.collect()
|
|
134
147
|
}
|
|
148
|
+
|
|
149
|
+
fn envelope_referenced_paths(prompt: &str) -> Option<Vec<String>> {
|
|
150
|
+
let input =
|
|
151
|
+
serde_json::from_str::<PromptEnvelopeContext>(prompt_envelope_json(prompt)?).ok()?;
|
|
152
|
+
if input.schema.as_deref() != Some("naome.prompt-envelope.v1") {
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
155
|
+
Some(
|
|
156
|
+
input
|
|
157
|
+
.referenced_paths
|
|
158
|
+
.unwrap_or_default()
|
|
159
|
+
.into_iter()
|
|
160
|
+
.map(|path| path.trim_start_matches("./").replace('\\', "/"))
|
|
161
|
+
.filter(|path| is_repo_relative_path(path))
|
|
162
|
+
.filter(|path| path_looks_contextual(path))
|
|
163
|
+
.collect(),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn is_repo_relative_path(path: &str) -> bool {
|
|
168
|
+
let candidate = Path::new(path);
|
|
169
|
+
!path.contains(':')
|
|
170
|
+
&& !candidate.is_absolute()
|
|
171
|
+
&& !candidate
|
|
172
|
+
.components()
|
|
173
|
+
.any(|component| matches!(component, Component::ParentDir))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn path_looks_contextual(path: &str) -> bool {
|
|
177
|
+
path.contains('/') || is_source_path(path) || path.ends_with(".md")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn path_exists_or_has_repo_parent(root: &Path, path: &str) -> bool {
|
|
181
|
+
let candidate = root.join(path);
|
|
182
|
+
if candidate.exists() {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
candidate
|
|
186
|
+
.parent()
|
|
187
|
+
.is_some_and(|parent| parent != root && parent.exists())
|
|
188
|
+
}
|
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
use std::collections::HashSet;
|
|
2
2
|
|
|
3
3
|
use super::envelope::parse_routing_envelope;
|
|
4
|
-
use super::model::{CanonicalIntent, IntentCandidate, IntentKind
|
|
5
|
-
use super::patterns;
|
|
4
|
+
use super::model::{CanonicalIntent, IntentCandidate, IntentKind};
|
|
6
5
|
use super::risk::detect_risk;
|
|
7
6
|
use super::segment::{actionable_text, segment_prompt};
|
|
8
7
|
|
|
9
|
-
const DIRECT_WORKFLOW_CONFIDENCE: u8 = 90;
|
|
10
|
-
const NEW_TASK_CONFIDENCE: u8 = 80;
|
|
11
|
-
const REVISION_CONFIDENCE: u8 = 78;
|
|
12
|
-
const GENERIC_WORK_CONFIDENCE: u8 = 50;
|
|
13
|
-
|
|
14
8
|
pub(crate) fn canonical_intent(prompt: &str) -> CanonicalIntent {
|
|
15
9
|
let segments = segment_prompt(prompt);
|
|
16
10
|
let actionable = actionable_text(&segments);
|
|
17
|
-
let mut
|
|
11
|
+
let mut has_routing_envelope = false;
|
|
12
|
+
let mut validation_errors = Vec::new();
|
|
13
|
+
let mut candidates = Vec::new();
|
|
18
14
|
let mut risk = detect_risk(&segments);
|
|
19
15
|
|
|
20
16
|
if let Some(envelope) = parse_routing_envelope(prompt) {
|
|
17
|
+
has_routing_envelope = true;
|
|
21
18
|
candidates.clear();
|
|
22
|
-
|
|
19
|
+
validation_errors = envelope.validation_errors;
|
|
23
20
|
risk.risk_codes.extend(envelope.risk.risk_codes);
|
|
24
21
|
risk.risk_codes.sort();
|
|
25
22
|
risk.risk_codes.dedup();
|
|
26
23
|
risk.has_risky_terms = !risk.risk_codes.is_empty();
|
|
24
|
+
if validation_errors.is_empty() {
|
|
25
|
+
candidates.extend(envelope.candidates);
|
|
26
|
+
} else {
|
|
27
|
+
candidates.push(IntentCandidate {
|
|
28
|
+
kind: IntentKind::PromptNormalizationRequired,
|
|
29
|
+
confidence: 100,
|
|
30
|
+
evidence: "invalid_prompt_envelope".to_string(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} else if !actionable.trim().is_empty() {
|
|
34
|
+
candidates.clear();
|
|
35
|
+
candidates.push(IntentCandidate {
|
|
36
|
+
kind: IntentKind::PromptNormalizationRequired,
|
|
37
|
+
confidence: 100,
|
|
38
|
+
evidence: "missing_prompt_envelope".to_string(),
|
|
39
|
+
});
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
candidates = dedupe_candidates(candidates);
|
|
@@ -37,11 +50,13 @@ pub(crate) fn canonical_intent(prompt: &str) -> CanonicalIntent {
|
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
CanonicalIntent {
|
|
40
|
-
is_empty: actionable.trim().is_empty(),
|
|
53
|
+
is_empty: actionable.trim().is_empty() && !has_routing_envelope,
|
|
54
|
+
has_routing_envelope,
|
|
41
55
|
references_current_task,
|
|
42
56
|
has_workflow_conflict: has_workflow_conflict(&candidates),
|
|
43
57
|
candidates,
|
|
44
58
|
risk,
|
|
59
|
+
validation_errors,
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
|
|
@@ -51,14 +66,16 @@ pub(crate) fn winning_intent(canonical: &CanonicalIntent) -> IntentKind {
|
|
|
51
66
|
}
|
|
52
67
|
for kind in [
|
|
53
68
|
IntentKind::Unsafe,
|
|
69
|
+
IntentKind::PromptNormalizationRequired,
|
|
54
70
|
IntentKind::StatusQuestion,
|
|
71
|
+
IntentKind::Advisory,
|
|
55
72
|
IntentKind::RepairRequest,
|
|
56
73
|
IntentKind::CancelRequest,
|
|
57
74
|
IntentKind::ReviewRequest,
|
|
58
75
|
IntentKind::NoCommitRequest,
|
|
59
76
|
IntentKind::CommitRequest,
|
|
60
|
-
IntentKind::NewTask,
|
|
61
77
|
IntentKind::TaskRevision,
|
|
78
|
+
IntentKind::NewTask,
|
|
62
79
|
IntentKind::TaskCompletion,
|
|
63
80
|
] {
|
|
64
81
|
if has_candidate(canonical, kind) {
|
|
@@ -75,67 +92,44 @@ pub(crate) fn has_candidate(canonical: &CanonicalIntent, kind: IntentKind) -> bo
|
|
|
75
92
|
.any(|candidate| candidate.kind == kind)
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
fn classify_candidates(segments: &[PromptSegment]) -> Vec<IntentCandidate> {
|
|
79
|
-
let mut candidates = Vec::new();
|
|
80
|
-
for segment in action_segments(segments) {
|
|
81
|
-
classify_direct_workflow(&mut candidates, segment);
|
|
82
|
-
classify_task_work(&mut candidates, &segment.text);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if candidates.is_empty() {
|
|
86
|
-
let actionable = actionable_text(segments);
|
|
87
|
-
if patterns::is_generic_work_request(&actionable) {
|
|
88
|
-
push(
|
|
89
|
-
&mut candidates,
|
|
90
|
-
IntentKind::NewTask,
|
|
91
|
-
GENERIC_WORK_CONFIDENCE,
|
|
92
|
-
"generic_work_request",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
dedupe_candidates(candidates)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
fn classify_direct_workflow(candidates: &mut Vec<IntentCandidate>, segment: &PromptSegment) {
|
|
101
|
-
let intents = if segment.kind == SegmentKind::CommandLike {
|
|
102
|
-
patterns::command_workflow_intents(&segment.text)
|
|
103
|
-
} else {
|
|
104
|
-
patterns::structured_workflow_intents(&segment.text)
|
|
105
|
-
};
|
|
106
|
-
for intent in intents {
|
|
107
|
-
push(
|
|
108
|
-
candidates,
|
|
109
|
-
intent,
|
|
110
|
-
DIRECT_WORKFLOW_CONFIDENCE,
|
|
111
|
-
&patterns::normalized(&segment.text),
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
fn classify_task_work(candidates: &mut Vec<IntentCandidate>, text: &str) {
|
|
117
|
-
if let Some(intent) = patterns::explicit_task_intent(text) {
|
|
118
|
-
let confidence = if intent == IntentKind::NewTask {
|
|
119
|
-
NEW_TASK_CONFIDENCE
|
|
120
|
-
} else {
|
|
121
|
-
REVISION_CONFIDENCE
|
|
122
|
-
};
|
|
123
|
-
push(candidates, intent, confidence, &patterns::normalized(text));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
95
|
fn has_candidate_kind(candidates: &[IntentCandidate], kind: IntentKind) -> bool {
|
|
128
96
|
candidates.iter().any(|candidate| candidate.kind == kind)
|
|
129
97
|
}
|
|
130
98
|
|
|
131
99
|
fn has_workflow_conflict(candidates: &[IntentCandidate]) -> bool {
|
|
100
|
+
let kinds = candidates
|
|
101
|
+
.iter()
|
|
102
|
+
.map(|candidate| candidate.kind)
|
|
103
|
+
.collect::<HashSet<_>>();
|
|
104
|
+
let mutating_kinds = [
|
|
105
|
+
IntentKind::NewTask,
|
|
106
|
+
IntentKind::TaskRevision,
|
|
107
|
+
IntentKind::CommitRequest,
|
|
108
|
+
IntentKind::RepairRequest,
|
|
109
|
+
IntentKind::ReviewRequest,
|
|
110
|
+
IntentKind::CancelRequest,
|
|
111
|
+
IntentKind::TaskCompletion,
|
|
112
|
+
];
|
|
113
|
+
if (kinds.contains(&IntentKind::Advisory) || kinds.contains(&IntentKind::StatusQuestion))
|
|
114
|
+
&& mutating_kinds.iter().any(|kind| kinds.contains(kind))
|
|
115
|
+
{
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if kinds.contains(&IntentKind::CommitRequest) && kinds.contains(&IntentKind::NoCommitRequest) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
132
122
|
let workflow = candidates
|
|
133
123
|
.iter()
|
|
134
|
-
.filter(|candidate| candidate.confidence >=
|
|
124
|
+
.filter(|candidate| candidate.confidence >= 90)
|
|
135
125
|
.filter(|candidate| {
|
|
136
126
|
!matches!(
|
|
137
127
|
candidate.kind,
|
|
138
|
-
IntentKind::NewTask
|
|
128
|
+
IntentKind::NewTask
|
|
129
|
+
| IntentKind::TaskRevision
|
|
130
|
+
| IntentKind::Unsafe
|
|
131
|
+
| IntentKind::Advisory
|
|
132
|
+
| IntentKind::StatusQuestion
|
|
139
133
|
)
|
|
140
134
|
})
|
|
141
135
|
.map(|candidate| candidate.kind)
|
|
@@ -143,25 +137,6 @@ fn has_workflow_conflict(candidates: &[IntentCandidate]) -> bool {
|
|
|
143
137
|
workflow.len() > 1
|
|
144
138
|
}
|
|
145
139
|
|
|
146
|
-
fn action_segments(segments: &[PromptSegment]) -> impl Iterator<Item = &PromptSegment> {
|
|
147
|
-
segments.iter().filter(|segment| {
|
|
148
|
-
matches!(
|
|
149
|
-
segment.kind,
|
|
150
|
-
super::model::SegmentKind::Prose
|
|
151
|
-
| super::model::SegmentKind::ListItem
|
|
152
|
-
| super::model::SegmentKind::CommandLike
|
|
153
|
-
)
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fn push(candidates: &mut Vec<IntentCandidate>, kind: IntentKind, confidence: u8, text: &str) {
|
|
158
|
-
candidates.push(IntentCandidate {
|
|
159
|
-
kind,
|
|
160
|
-
confidence,
|
|
161
|
-
evidence: text.to_string(),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
140
|
fn dedupe_candidates(candidates: Vec<IntentCandidate>) -> Vec<IntentCandidate> {
|
|
166
141
|
let mut seen = HashSet::new();
|
|
167
142
|
candidates
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
use serde::Deserialize;
|
|
2
|
+
use serde_json::Value;
|
|
2
3
|
|
|
3
4
|
use super::model::{IntentCandidate, IntentKind, RiskContext};
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const PROMPT_ENVELOPE_FENCE: &str = "```naome-prompt-envelope-v1";
|
|
6
7
|
|
|
7
8
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
8
9
|
pub(crate) struct RoutingEnvelope {
|
|
9
10
|
pub candidates: Vec<IntentCandidate>,
|
|
10
11
|
pub risk: RiskContext,
|
|
12
|
+
pub validation_errors: Vec<String>,
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
#[derive(Debug, Deserialize)]
|
|
14
16
|
#[serde(rename_all = "camelCase")]
|
|
15
17
|
struct EnvelopeInput {
|
|
16
18
|
schema: Option<String>,
|
|
19
|
+
request_kind: Option<String>,
|
|
20
|
+
mutation_intent: Option<String>,
|
|
21
|
+
publication_intent: Option<String>,
|
|
22
|
+
requested_actions: Option<Vec<String>>,
|
|
17
23
|
workflow_action: Option<String>,
|
|
18
24
|
task_intent: Option<String>,
|
|
19
25
|
risk: Option<String>,
|
|
@@ -21,32 +27,84 @@ struct EnvelopeInput {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
pub(crate) fn parse_routing_envelope(prompt: &str) -> Option<RoutingEnvelope> {
|
|
24
|
-
let json =
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
let json = prompt_envelope_json(prompt)?;
|
|
31
|
+
let raw = match serde_json::from_str::<Value>(json) {
|
|
32
|
+
Ok(raw) => raw,
|
|
33
|
+
Err(_) => {
|
|
34
|
+
return Some(invalid_envelope(
|
|
35
|
+
vec!["prompt_envelope_invalid:json".to_string()],
|
|
36
|
+
RiskContext {
|
|
37
|
+
has_risky_terms: false,
|
|
38
|
+
risk_codes: Vec::new(),
|
|
39
|
+
},
|
|
40
|
+
));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
let input = match serde_json::from_value::<EnvelopeInput>(raw.clone()) {
|
|
44
|
+
Ok(input) => input,
|
|
45
|
+
Err(_) => {
|
|
46
|
+
let mut validation_errors = vec!["prompt_envelope_invalid:shape".to_string()];
|
|
47
|
+
let risk_codes = envelope_risk_codes(
|
|
48
|
+
raw.get("risk").and_then(Value::as_str),
|
|
49
|
+
raw_string_array(&raw, "riskCodes"),
|
|
50
|
+
&mut validation_errors,
|
|
51
|
+
);
|
|
52
|
+
return Some(invalid_envelope(
|
|
53
|
+
validation_errors,
|
|
54
|
+
RiskContext {
|
|
55
|
+
has_risky_terms: !risk_codes.is_empty(),
|
|
56
|
+
risk_codes,
|
|
57
|
+
},
|
|
58
|
+
));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
29
61
|
|
|
30
62
|
let mut candidates = Vec::new();
|
|
31
|
-
|
|
63
|
+
let mut validation_errors = Vec::new();
|
|
64
|
+
if input.schema.as_deref() != Some("naome.prompt-envelope.v1") {
|
|
65
|
+
validation_errors.push("prompt_envelope_invalid:schema".to_string());
|
|
66
|
+
}
|
|
67
|
+
if let Some(kind) = request_kind(input.request_kind.as_deref(), &mut validation_errors) {
|
|
68
|
+
candidates.push(candidate(kind, "envelope.requestKind"));
|
|
69
|
+
}
|
|
70
|
+
if let Some(kind) = mutation_intent(input.mutation_intent.as_deref(), &mut validation_errors) {
|
|
71
|
+
candidates.push(candidate(kind, "envelope.mutationIntent"));
|
|
72
|
+
}
|
|
73
|
+
if let Some(kind) =
|
|
74
|
+
publication_intent(input.publication_intent.as_deref(), &mut validation_errors)
|
|
75
|
+
{
|
|
76
|
+
candidates.push(candidate(kind, "envelope.publicationIntent"));
|
|
77
|
+
}
|
|
78
|
+
for kind in requested_actions(input.requested_actions, &mut validation_errors) {
|
|
79
|
+
candidates.push(candidate(kind, "envelope.requestedActions"));
|
|
80
|
+
}
|
|
81
|
+
if let Some(kind) = workflow_action(input.workflow_action.as_deref(), &mut validation_errors) {
|
|
32
82
|
candidates.push(candidate(kind, "envelope.workflowAction"));
|
|
33
83
|
}
|
|
34
|
-
if let Some(kind) = task_intent(input.task_intent.as_deref()) {
|
|
84
|
+
if let Some(kind) = task_intent(input.task_intent.as_deref(), &mut validation_errors) {
|
|
35
85
|
candidates.push(candidate(kind, "envelope.taskIntent"));
|
|
36
86
|
}
|
|
37
87
|
|
|
38
|
-
let risk_codes = envelope_risk_codes(
|
|
88
|
+
let risk_codes = envelope_risk_codes(
|
|
89
|
+
input.risk.as_deref(),
|
|
90
|
+
input.risk_codes,
|
|
91
|
+
&mut validation_errors,
|
|
92
|
+
);
|
|
39
93
|
let risk = RiskContext {
|
|
40
94
|
has_risky_terms: !risk_codes.is_empty(),
|
|
41
95
|
risk_codes,
|
|
42
96
|
};
|
|
43
97
|
|
|
44
|
-
Some(RoutingEnvelope {
|
|
98
|
+
Some(RoutingEnvelope {
|
|
99
|
+
candidates,
|
|
100
|
+
risk,
|
|
101
|
+
validation_errors,
|
|
102
|
+
})
|
|
45
103
|
}
|
|
46
104
|
|
|
47
|
-
fn
|
|
48
|
-
let
|
|
49
|
-
let after_open =
|
|
105
|
+
pub(crate) fn prompt_envelope_json(prompt: &str) -> Option<&str> {
|
|
106
|
+
let fence = PROMPT_ENVELOPE_FENCE;
|
|
107
|
+
let after_open = prompt.strip_prefix(fence)?;
|
|
50
108
|
let after_line = after_open
|
|
51
109
|
.strip_prefix('\n')
|
|
52
110
|
.or_else(|| after_open.strip_prefix("\r\n"))?;
|
|
@@ -54,7 +112,69 @@ fn envelope_json(prompt: &str) -> Option<&str> {
|
|
|
54
112
|
Some(after_line[..end].trim())
|
|
55
113
|
}
|
|
56
114
|
|
|
57
|
-
fn
|
|
115
|
+
fn request_kind(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
116
|
+
match normalized_value(value)?.as_str() {
|
|
117
|
+
"none" | "unknown" => None,
|
|
118
|
+
"advisory" | "planning" | "planning_only" | "question" | "recommendation" => {
|
|
119
|
+
Some(IntentKind::Advisory)
|
|
120
|
+
}
|
|
121
|
+
"implementation" | "execution" | "task" | "feature" | "fix" => Some(IntentKind::NewTask),
|
|
122
|
+
"revision" | "correction" => Some(IntentKind::TaskRevision),
|
|
123
|
+
"status" => Some(IntentKind::StatusQuestion),
|
|
124
|
+
_ => invalid_value("requestKind", errors),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn mutation_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
129
|
+
match normalized_value(value)?.as_str() {
|
|
130
|
+
"none" | "unknown" => None,
|
|
131
|
+
"modify_files" | "edit_files" | "create_files" | "delete_files" => {
|
|
132
|
+
Some(IntentKind::NewTask)
|
|
133
|
+
}
|
|
134
|
+
"commit" | "baseline" => Some(IntentKind::CommitRequest),
|
|
135
|
+
"publish" | "push" | "merge_request" | "pull_request" => {
|
|
136
|
+
unsupported_value("mutationIntent", errors)
|
|
137
|
+
}
|
|
138
|
+
_ => invalid_value("mutationIntent", errors),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn publication_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
143
|
+
match normalized_value(value)?.as_str() {
|
|
144
|
+
"none" | "unknown" => None,
|
|
145
|
+
"commit" => Some(IntentKind::CommitRequest),
|
|
146
|
+
"push" | "merge_request" | "pull_request" | "mr" | "pr" => {
|
|
147
|
+
unsupported_value("publicationIntent", errors)
|
|
148
|
+
}
|
|
149
|
+
_ => invalid_value("publicationIntent", errors),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn requested_actions(values: Option<Vec<String>>, errors: &mut Vec<String>) -> Vec<IntentKind> {
|
|
154
|
+
values
|
|
155
|
+
.unwrap_or_default()
|
|
156
|
+
.into_iter()
|
|
157
|
+
.filter_map(|value| action_token(&value, errors))
|
|
158
|
+
.collect()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn action_token(value: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
162
|
+
match normalized_value(Some(value))?.as_str() {
|
|
163
|
+
"answer" | "advise" | "plan" | "recommend" => Some(IntentKind::Advisory),
|
|
164
|
+
"implement" | "edit" | "modify" | "create_task" => Some(IntentKind::NewTask),
|
|
165
|
+
"revise" | "correct" => Some(IntentKind::TaskRevision),
|
|
166
|
+
"status" => Some(IntentKind::StatusQuestion),
|
|
167
|
+
"review" => Some(IntentKind::ReviewRequest),
|
|
168
|
+
"repair" => Some(IntentKind::RepairRequest),
|
|
169
|
+
"commit" | "baseline" => Some(IntentKind::CommitRequest),
|
|
170
|
+
"no_commit" => Some(IntentKind::NoCommitRequest),
|
|
171
|
+
"cancel" => Some(IntentKind::CancelRequest),
|
|
172
|
+
"complete" => Some(IntentKind::TaskCompletion),
|
|
173
|
+
_ => invalid_value("requestedActions", errors),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn workflow_action(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
58
178
|
match normalized_value(value)?.as_str() {
|
|
59
179
|
"none" | "unknown" => None,
|
|
60
180
|
"commit_request" => Some(IntentKind::CommitRequest),
|
|
@@ -64,20 +184,24 @@ fn workflow_action(value: Option<&str>) -> Option<IntentKind> {
|
|
|
64
184
|
"task_completion" => Some(IntentKind::TaskCompletion),
|
|
65
185
|
"status_question" => Some(IntentKind::StatusQuestion),
|
|
66
186
|
"no_commit_request" => Some(IntentKind::NoCommitRequest),
|
|
67
|
-
_ =>
|
|
187
|
+
_ => invalid_value("workflowAction", errors),
|
|
68
188
|
}
|
|
69
189
|
}
|
|
70
190
|
|
|
71
|
-
fn task_intent(value: Option<&str>) -> Option<IntentKind> {
|
|
191
|
+
fn task_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
72
192
|
match normalized_value(value)?.as_str() {
|
|
73
193
|
"none" | "unknown" => None,
|
|
74
194
|
"new_task" => Some(IntentKind::NewTask),
|
|
75
195
|
"task_revision" => Some(IntentKind::TaskRevision),
|
|
76
|
-
_ =>
|
|
196
|
+
_ => invalid_value("taskIntent", errors),
|
|
77
197
|
}
|
|
78
198
|
}
|
|
79
199
|
|
|
80
|
-
fn envelope_risk_codes(
|
|
200
|
+
fn envelope_risk_codes(
|
|
201
|
+
risk: Option<&str>,
|
|
202
|
+
risk_codes: Option<Vec<String>>,
|
|
203
|
+
errors: &mut Vec<String>,
|
|
204
|
+
) -> Vec<String> {
|
|
81
205
|
let mut codes = risk_codes.unwrap_or_default();
|
|
82
206
|
match normalized_value(risk).as_deref() {
|
|
83
207
|
Some("none") | Some("unknown") | None => {}
|
|
@@ -86,19 +210,49 @@ fn envelope_risk_codes(risk: Option<&str>, risk_codes: Option<Vec<String>>) -> V
|
|
|
86
210
|
codes.push("envelope_risk:credential_context".to_string());
|
|
87
211
|
}
|
|
88
212
|
Some("bypass_context") => codes.push("envelope_risk:bypass_context".to_string()),
|
|
89
|
-
Some(
|
|
213
|
+
Some(_) => {
|
|
214
|
+
errors.push("prompt_envelope_invalid:risk".to_string());
|
|
215
|
+
}
|
|
90
216
|
}
|
|
91
217
|
codes.sort();
|
|
92
218
|
codes.dedup();
|
|
93
219
|
codes
|
|
94
220
|
}
|
|
95
221
|
|
|
222
|
+
fn invalid_value(field: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
223
|
+
errors.push(format!("prompt_envelope_invalid:{field}"));
|
|
224
|
+
None
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn unsupported_value(field: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
|
|
228
|
+
errors.push(format!("prompt_envelope_unsupported:{field}"));
|
|
229
|
+
None
|
|
230
|
+
}
|
|
231
|
+
|
|
96
232
|
fn normalized_value(value: Option<&str>) -> Option<String> {
|
|
97
233
|
value
|
|
98
234
|
.map(|value| value.trim().to_ascii_lowercase())
|
|
99
235
|
.filter(|value| !value.is_empty())
|
|
100
236
|
}
|
|
101
237
|
|
|
238
|
+
fn raw_string_array(raw: &Value, field: &str) -> Option<Vec<String>> {
|
|
239
|
+
raw.get(field).and_then(Value::as_array).map(|values| {
|
|
240
|
+
values
|
|
241
|
+
.iter()
|
|
242
|
+
.filter_map(Value::as_str)
|
|
243
|
+
.map(ToString::to_string)
|
|
244
|
+
.collect()
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn invalid_envelope(validation_errors: Vec<String>, risk: RiskContext) -> RoutingEnvelope {
|
|
249
|
+
RoutingEnvelope {
|
|
250
|
+
candidates: Vec::new(),
|
|
251
|
+
risk,
|
|
252
|
+
validation_errors,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
102
256
|
fn candidate(kind: IntentKind, evidence: &str) -> IntentCandidate {
|
|
103
257
|
IntentCandidate {
|
|
104
258
|
kind,
|
|
@@ -19,6 +19,8 @@ pub(crate) fn response_policy(
|
|
|
19
19
|
"auto_commit_harness_refresh_then_completed_task_then_create_new_task" => "The completed task is valid after separating deterministic harness refresh changes. NAOME can baseline the refresh first, then baseline the task before starting the next separate task.",
|
|
20
20
|
"auto_commit_upgrade_baseline_then_create_new_task" => "The setup or repair diff is ready. NAOME can baseline it automatically before starting the next task.",
|
|
21
21
|
"create_new_task" | "create_new_task_without_auto_baseline" => "NAOME is ready to create the next task.",
|
|
22
|
+
"normalize_prompt_first" => "NAOME needs a structured prompt envelope before it can route or mutate repository state.",
|
|
23
|
+
"answer_without_mutation" => "The prompt is advisory or planning-only, so NAOME will not mutate repository state.",
|
|
22
24
|
"answer_status_only" => summary,
|
|
23
25
|
"block_auto_baseline_due_to_no_commit" => "I will not baseline or commit because the prompt explicitly blocks committing. Review, revise, cancel, or clear the current diff first.",
|
|
24
26
|
"review_task_diff" | "review_diff_first" | "review_current_task_diff" => "The prompt asks for review, so NAOME will not mutate the repository automatically.",
|
|
@@ -18,6 +18,8 @@ pub(crate) struct PromptSegment {
|
|
|
18
18
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
19
19
|
pub(crate) enum IntentKind {
|
|
20
20
|
Ambiguous,
|
|
21
|
+
PromptNormalizationRequired,
|
|
22
|
+
Advisory,
|
|
21
23
|
NewTask,
|
|
22
24
|
TaskRevision,
|
|
23
25
|
StatusQuestion,
|
|
@@ -34,6 +36,8 @@ impl IntentKind {
|
|
|
34
36
|
pub fn as_str(self) -> &'static str {
|
|
35
37
|
match self {
|
|
36
38
|
Self::Ambiguous => "ambiguous",
|
|
39
|
+
Self::PromptNormalizationRequired => "prompt_normalization_required",
|
|
40
|
+
Self::Advisory => "advisory",
|
|
37
41
|
Self::NewTask => "new_task",
|
|
38
42
|
Self::TaskRevision => "task_revision",
|
|
39
43
|
Self::StatusQuestion => "status_question",
|
|
@@ -64,8 +68,10 @@ pub(crate) struct RiskContext {
|
|
|
64
68
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
65
69
|
pub(crate) struct CanonicalIntent {
|
|
66
70
|
pub is_empty: bool,
|
|
71
|
+
pub has_routing_envelope: bool,
|
|
67
72
|
pub references_current_task: bool,
|
|
68
73
|
pub candidates: Vec<IntentCandidate>,
|
|
69
74
|
pub risk: RiskContext,
|
|
70
75
|
pub has_workflow_conflict: bool,
|
|
76
|
+
pub validation_errors: Vec<String>,
|
|
71
77
|
}
|