@dot-agent/language-server 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,9 +19,9 @@
19
19
  const { CompletionItemKind } = require('vscode-languageserver');
20
20
  const { nodesOfType, positionToOffset, getContextNode } = require('../parser');
21
21
 
22
- const BEHAVIOR_TOP_KW = ['state', 'merge', 'on event', 'on intent', 'on offtopic', 'on fallback'];
23
- const BEHAVIOR_BLOCK_KW = ['guide', 'teach', 'goal', 'interact', 'run', 'transition', 'set', 'if', 'else', 'after', 'parallel', 'apply', 'remove', 'on intent', 'on offtopic', 'on fallback', 'on complete', 'on failed'];
24
- const AGENT_TOP_KW = ['agent', 'domain', 'license', 'terms', 'privacy', 'description', 'behavior', 'requires', 'input', 'capabilities', 'output', 'type', 'concept', 'schema'];
22
+ const BEHAVIOR_TOP_KW = ['state', 'merge', 'on event', 'on intent', 'on offtopic'];
23
+ const BEHAVIOR_BLOCK_KW = ['guide', 'teach', 'goal', 'interact', 'run', 'transition', 'set', 'if', 'else', 'end', 'after', 'parallel', 'apply', 'remove', 'on intent', 'on offtopic', 'on failure', 'on success'];
24
+ const DESCRIPTION_TOP_KW = ['agent', 'domain', 'license', 'terms', 'privacy', 'description', 'behavior', 'requires', 'input', 'capabilities', 'output', 'type', 'concept'];
25
25
  const STRICT_BLOCKS = new Set(['input_block', 'output_block', 'requires_block', 'capabilities_block']);
26
26
 
27
27
  function kw(label) {
@@ -59,7 +59,7 @@ function provideCompletions(langId, tree, text, position) {
59
59
  return ['script', 'subagent', 'tool'].map(kw);
60
60
  }
61
61
  if (/\bon\s+\S*$/.test(before)) {
62
- return ['event', 'intent', 'offtopic', 'fallback', 'complete', 'failed'].map(kw);
62
+ return ['event', 'intent', 'offtopic', 'failure', 'success'].map(kw);
63
63
  }
64
64
 
65
65
  // Context-aware: top-level vs. inside a block
@@ -72,9 +72,9 @@ function provideCompletions(langId, tree, text, position) {
72
72
  return (/^\s/.test(line) ? BEHAVIOR_BLOCK_KW : BEHAVIOR_TOP_KW).map(kw);
73
73
  }
74
74
 
75
- if (langId === 'agent') {
75
+ if (langId === 'description') {
76
76
  if (!/^\s/.test(line)) {
77
- return AGENT_TOP_KW.map(kw);
77
+ return DESCRIPTION_TOP_KW.map(kw);
78
78
  }
79
79
 
80
80
  // Inside a strict block → suggest declared types
@@ -29,7 +29,7 @@ function provideDefinition(langId, tree, text, uri, position) {
29
29
  if (langId === 'behavior') {
30
30
  targetNode = nodesOfType(tree, 'state_decl')
31
31
  .find(n => n.childForFieldName('name')?.text === word);
32
- } else if (langId === 'agent') {
32
+ } else if (langId === 'description') {
33
33
  if (!/^[A-Z]/.test(word) && !word.includes('.')) return null;
34
34
  targetNode = nodesOfType(tree, 'type_decl')
35
35
  .find(n => n.childForFieldName('name')?.text === word);
@@ -26,7 +26,7 @@ const STRICT_BLOCK_TYPES = {
26
26
  capabilities_block: 'capabilities',
27
27
  };
28
28
 
29
- function diagnoseAgent(tree, text) {
29
+ function diagnoseDescription(tree, text) {
30
30
  const diagnostics = [];
31
31
 
32
32
  // ── Strict block validation ───────────────────────────────────────────────
@@ -44,7 +44,7 @@ function diagnoseAgent(tree, text) {
44
44
  range: nodeToRange(errorNode),
45
45
  message: `Syntax error in '${blockName}' block. Expected: Type or Type "Description", or compact: Type1, Type2.`,
46
46
  severity: DiagnosticSeverity.Error,
47
- source: 'agent-dsl',
47
+ source: 'description-dsl',
48
48
  });
49
49
  }
50
50
 
@@ -59,7 +59,7 @@ function diagnoseAgent(tree, text) {
59
59
  range: nodeToRange(idNode),
60
60
  message: `Type '${typeName}' is not declared in this file (assuming native or external).`,
61
61
  severity: DiagnosticSeverity.Warning,
62
- source: 'agent-dsl',
62
+ source: 'description-dsl',
63
63
  });
64
64
  }
65
65
  }
@@ -70,7 +70,7 @@ function diagnoseAgent(tree, text) {
70
70
  return diagnostics;
71
71
  }
72
72
 
73
- function diagnoseFlow(tree) {
73
+ function diagnoseBehavior(tree) {
74
74
  const diagnostics = [];
75
75
 
76
76
  // ── Rule 1: Dangling transitions ──────────────────────────────────────────
@@ -100,18 +100,6 @@ function diagnoseFlow(tree) {
100
100
  for (const n of nodesOfType(tree, 'transition_stmt')) {
101
101
  checkTransitionTarget(n.childForFieldName('state'));
102
102
  }
103
- for (const n of nodesOfType(tree, 'intent_trigger')) {
104
- // inline form: on intent "..." transition to <state>
105
- checkTransitionTarget(n.childForFieldName('state'));
106
- }
107
- for (const n of nodesOfType(tree, 'fallback_stmt')) {
108
- // inline form: on fallback transition to <state>
109
- checkTransitionTarget(n.childForFieldName('state'));
110
- }
111
- for (const n of nodesOfType(tree, 'offtopic_stmt')) {
112
- // inline form: on offtopic transition to <state>
113
- checkTransitionTarget(n.childForFieldName('state'));
114
- }
115
103
 
116
104
  // ── Rule 2: Dead-end interact (now redundant but kept for safety) ────────────
117
105
  // With the new grammar, oriented_state_body requires repeat1(handlers),
@@ -125,14 +113,13 @@ function diagnoseFlow(tree) {
125
113
 
126
114
  // With new grammar: oriented_state_body always has handlers after interact (repeat1)
127
115
  // So this check should never trigger, but kept for robustness
128
- const hasHandlers = ancestor.descendantsOfType('intent_trigger').length +
129
- ancestor.descendantsOfType('fallback_stmt').length +
130
- ancestor.descendantsOfType('offtopic_stmt').length > 0;
116
+ const hasHandlers = ancestor.descendantsOfType('intent_handler').length +
117
+ ancestor.descendantsOfType('offtopic_handler').length > 0;
131
118
 
132
119
  if (!hasHandlers) {
133
120
  diagnostics.push({
134
121
  range: nodeToRange(interactNode),
135
- message: "This state calls interact but has no handlers (on intent/fallback/offtopic). This will trap the agent.",
122
+ message: "This state calls interact but has no handlers (on intent/offtopic). This will trap the agent.",
136
123
  severity: DiagnosticSeverity.Warning,
137
124
  source: 'behavior-dsl',
138
125
  });
@@ -144,8 +131,8 @@ function diagnoseFlow(tree) {
144
131
 
145
132
  function diagnose(langId, tree, text) {
146
133
  if (!tree) return [];
147
- if (langId === 'agent') return diagnoseAgent(tree, text);
148
- if (langId === 'behavior') return diagnoseFlow(tree);
134
+ if (langId === 'description') return diagnoseDescription(tree, text);
135
+ if (langId === 'behavior') return diagnoseBehavior(tree);
149
136
  return [];
150
137
  }
151
138
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  'use strict';
18
18
 
19
- const BLOCK_HEADERS = /^(on\s+(intent|offtopic|fallback|complete|failed)|if|else|after|parallel)\b/;
19
+ const BLOCK_HEADERS = /^(on\s+(intent|offtopic|failure|success)|if|else|after|parallel)\b/;
20
20
  const TOP_LEVEL_LINE = /^(state|merge)\s|^on\s+event\b/;
21
21
 
22
22
  function formatBehavior(text) {
@@ -39,7 +39,12 @@ function formatBehavior(text) {
39
39
  if (BLOCK_HEADERS.test(trimmed)) mode = 'NESTED_BODY';
40
40
  expectedIndent = 2;
41
41
  } else {
42
- expectedIndent = BLOCK_HEADERS.test(trimmed) ? 2 : 4;
42
+ if (/^end\b/.test(trimmed)) {
43
+ mode = 'STATE_BODY';
44
+ expectedIndent = 2;
45
+ } else {
46
+ expectedIndent = BLOCK_HEADERS.test(trimmed) ? 2 : 4;
47
+ }
43
48
  }
44
49
 
45
50
  const actualIndent = raw.length - raw.trimStart().length;
@@ -53,7 +58,7 @@ function formatBehavior(text) {
53
58
  return edits;
54
59
  }
55
60
 
56
- function formatAgent(text) {
61
+ function formatDescription(text) {
57
62
  const edits = [];
58
63
  const lines = text.split('\n');
59
64
  for (let i = 0; i < lines.length; i++) {
@@ -73,7 +78,7 @@ function formatAgent(text) {
73
78
 
74
79
  function format(langId, text) {
75
80
  if (langId === 'behavior') return formatBehavior(text);
76
- if (langId === 'agent') return formatAgent(text);
81
+ if (langId === 'description') return formatDescription(text);
77
82
  return [];
78
83
  }
79
84
 
package/features/hover.js CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  const { MarkupKind } = require('vscode-languageserver');
20
20
 
21
- const AGENT_DOCS = {
21
+ const DESCRIPTION_DOCS = {
22
22
  'agent': '**`agent name`**\n\nDeclares a new agent. The central node of the manifest.',
23
23
  'domain': '**`domain url`**\n\nDeclares the canonical domain for this agent, establishing cryptographic identity and ownership.',
24
24
  'license': '**`license type`**\n\nDeclares the license under which this agent is distributed (e.g., MIT, Copyright).',
@@ -32,30 +32,32 @@ const AGENT_DOCS = {
32
32
  'output': '**`output Type`**\n\nThe data type this agent returns.',
33
33
  'type': '**`type name`**\n\nDeclares a custom type to anchor custom typing to Wikidata or Schema.org.',
34
34
  'concept': '**`concept url`**\n\nThe Wikidata or Schema.org concept URL this type maps to.',
35
- 'schema': '**`schema file.json`**\n\nA JSON schema file for this type.',
36
35
  };
37
36
 
38
37
  const BEHAVIOR_DOCS = {
39
38
  'merge': '**`merge "file.behavior"`**\n\nIncludes another `.behavior` file. Must appear before any `state` or `on event` declarations (preamble-only, eager loading).',
40
39
  'state': '**`state name`**\n\nDeclares a named state. States contain the logic that runs while the agent is in that state.',
41
- 'on': '**`on event|intent|offtopic|fallback|complete|failed`**\n\nBinds a handler to a trigger. Top-level: `on event`. Inside a state: `on intent`, `on offtopic`, `on fallback`. After parallel: `on complete`, `on failed`.',
42
- 'run': '**`run script|subagent|tool "target"`**\n\nExecutes a script, subagent, or tool. Accepts optional modifiers: `silent`, `in background`, `each collection`.',
40
+ 'on': '**`on event|intent|offtopic|failure|success`**\n\nBinds a handler to a trigger. Top-level: `on event`. Inside a state: `on intent`, `on offtopic`. After `run`/`apply`/`remove`: `on failure`. Inside `parallel`: `on success` (optional), `on failure` (required).',
41
+ 'run': '**`run script|subagent|tool "target" ["parameters"]`**\n\nExecutes a script, subagent, or tool. Optionally followed by `on failure` to handle errors.',
43
42
  'guide': '**`guide "text"`**\n\nInjects a system-level instruction into the conversation context, shaping the agent\'s persona or approach without being visible as a reply.',
44
- 'teach': '**`teach "text"`**\n\nAdds a fact or constraint to the agent\'s working knowledge for the duration of this state.',
43
+ 'teach': '**`teach "file"`**\n\nLoads a file into the agent\'s working knowledge for the duration of this state.',
45
44
  'goal': '**`goal "text"`**\n\nSets the agent\'s objective for this state, used by the runtime for planning and alignment checks.',
46
- 'interact': '**`interact [requiring "text"]`**\n\nPauses execution and waits for user input. Optionally enforces a requirement before continuing.',
45
+ 'interact': '**`interact`**\n\nPauses execution and waits for user input.',
47
46
  'set': '**`set domain.var = value`**\n\nAssigns a value to a memory variable. Domains: `context`, `session`, `worksession`, `user`.',
48
47
  'context': '**`context`** memory domain — scoped to the current agent run.',
49
48
  'session': '**`session`** memory domain — persists for the user\'s current session.',
50
49
  'worksession': '**`worksession`** memory domain — persists across a task-oriented work session (isolated per task).',
51
50
  'user': '**`user`** memory domain — persists across sessions for a given user.',
52
51
  'transition': '**`transition to state`**\n\nTransitions immediately to the named state.',
53
- 'if': '**`if condition`**\n\nConditional execution. Condition can use `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`.',
54
- 'else': '**`else`**\n\nAlternative branch of an `if` statement.',
52
+ 'if': '**`if condition`**\n\nConditional execution. Condition can use `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`. Close the block with `end`.',
53
+ 'else': '**`else`**\n\nAlternative branch of an `if` statement. Close the block with `end`.',
54
+ 'end': '**`end`**\n\nCloses an `if`/`else` block.',
55
55
  'after': '**`after N prompts`**\n\n[Experimental] Executes a block after N user prompts have occurred in this state.',
56
- 'parallel': '**`parallel`**\n\n[Experimental] Runs a block of `run` statements concurrently. Follow with `on complete` and `on failed` handlers.',
57
- 'apply': '**`apply css|html|video "text"`**\n\nApplies a UI manipulation to a CSS selector, HTML element, or video element.',
58
- 'remove': '**`remove css|html|video "text"`**\n\nRemoves a UI element by selector or reference.',
56
+ 'parallel': '**`parallel`**\n\n[Experimental] Runs a block of `run` statements concurrently. Follow with optional `on success` and required `on failure` handlers.',
57
+ 'apply': '**`apply css "selector"`**\n\nApplies a UI manipulation to a CSS selector. Optionally followed by `on failure`.',
58
+ 'remove': '**`remove css "selector"`**\n\nRemoves a UI element by CSS selector. Optionally followed by `on failure`.',
59
+ 'failure': '**`on failure`**\n\nError handler block executed when a `run`, `apply`, `remove`, or `parallel` statement fails.',
60
+ 'success': '**`on success`**\n\nOptional success handler block inside a `parallel` statement.',
59
61
  };
60
62
 
61
63
  function provideHover(langId, text, position) {
@@ -70,7 +72,7 @@ function provideHover(langId, text, position) {
70
72
  const word = line.slice(start, end);
71
73
  if (!word) return null;
72
74
 
73
- const docs = langId === 'agent' ? AGENT_DOCS : BEHAVIOR_DOCS;
75
+ const docs = langId === 'description' ? DESCRIPTION_DOCS : BEHAVIOR_DOCS;
74
76
  const doc = docs[word];
75
77
  if (!doc) return null;
76
78
 
package/features/links.js CHANGED
@@ -41,15 +41,11 @@ function provideDocumentLinks(langId, tree, docUri) {
41
41
  });
42
42
  }
43
43
 
44
- if (langId === 'agent') {
44
+ if (langId === 'description') {
45
45
  for (const node of nodesOfType(tree, 'behavior_block')) {
46
46
  const fileNode = node.childForFieldName('file');
47
47
  if (fileNode) addLink(fileNode, fileNode.text);
48
48
  }
49
- for (const node of nodesOfType(tree, 'schema_prop')) {
50
- const fileNode = node.childForFieldName('file');
51
- if (fileNode) addLink(fileNode, fileNode.text);
52
- }
53
49
  }
54
50
 
55
51
  if (langId === 'behavior') {
@@ -58,7 +54,7 @@ function provideDocumentLinks(langId, tree, docUri) {
58
54
  if (pathNode) addLink(pathNode, pathNode.text);
59
55
  }
60
56
  for (const node of nodesOfType(tree, 'run_stmt')) {
61
- const runTypeNode = node.childForFieldName('run_type');
57
+ const runTypeNode = node.childForFieldName('type');
62
58
  if (runTypeNode?.text !== 'script') continue;
63
59
  const targetNode = node.childForFieldName('target');
64
60
  if (targetNode) addLink(targetNode, targetNode.text);
@@ -41,12 +41,7 @@ function provideReferences(langId, tree, text, uri, position) {
41
41
  const stateNode = n.childForFieldName('state');
42
42
  if (stateNode?.text === word) add(stateNode);
43
43
  }
44
- // Inline intent handlers: on intent "..." next <state>
45
- for (const n of nodesOfType(tree, 'intent_trigger')) {
46
- const stateNode = n.childForFieldName('state');
47
- if (stateNode?.text === word) add(stateNode);
48
- }
49
- } else if (langId === 'agent') {
44
+ } else if (langId === 'description') {
50
45
  // Declaration
51
46
  for (const n of nodesOfType(tree, 'type_decl')) {
52
47
  const nameNode = n.childForFieldName('name');
@@ -41,12 +41,7 @@ function provideRenameEdits(langId, tree, text, uri, position, newName) {
41
41
  const stateNode = n.childForFieldName('state');
42
42
  if (stateNode?.text === oldName) addEdit(stateNode);
43
43
  }
44
- // Rename inline intent handler targets
45
- for (const n of nodesOfType(tree, 'intent_trigger')) {
46
- const stateNode = n.childForFieldName('state');
47
- if (stateNode?.text === oldName) addEdit(stateNode);
48
- }
49
- } else if (langId === 'agent') {
44
+ } else if (langId === 'description') {
50
45
  // Rename type declarations
51
46
  for (const n of nodesOfType(tree, 'type_decl')) {
52
47
  const nameNode = n.childForFieldName('name');
@@ -49,7 +49,7 @@ function provideDocumentSymbols(langId, tree) {
49
49
  }
50
50
  }
51
51
 
52
- if (langId === 'agent') {
52
+ if (langId === 'description') {
53
53
  for (const node of nodesOfType(tree, 'agent_decl')) {
54
54
  const nameNode = node.childForFieldName('name');
55
55
  if (!nameNode) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dot-agent/language-server",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "LSP server for .agent DSL files",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Danilo Borges <contato@daniloborg.es>",
@@ -27,10 +27,10 @@
27
27
  "start": "node server.js --stdio"
28
28
  },
29
29
  "dependencies": {
30
- "@dot-agent/tree-sitter": "^0.3.2",
31
- "vscode-languageserver": "^9.0.1",
30
+ "@dot-agent/tree-sitter": "^0.3.4",
31
+ "vscode-languageserver": "^10.0.0",
32
32
  "vscode-languageserver-textdocument": "^1.0.12",
33
- "web-tree-sitter": "^0.25.10"
33
+ "web-tree-sitter": "^0.26.9"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
package/parser.js CHANGED
@@ -21,17 +21,17 @@ const { Parser, Language } = require('web-tree-sitter');
21
21
  const grammar = require('@dot-agent/tree-sitter');
22
22
 
23
23
  // path.resolve ensures absolute paths survive vsix packaging and cwd changes
24
- const AGENT_WASM = path.resolve(grammar.agentWasmPath);
24
+ const DESCRIPTION_WASM = path.resolve(grammar.agentWasmPath);
25
25
  const BEHAVIOR_WASM = path.resolve(grammar.behaviorWasmPath);
26
26
 
27
- let agentParser, behaviorParser;
27
+ let descriptionParser, behaviorParser;
28
28
 
29
29
  async function initParsers() {
30
30
  await Parser.init();
31
- const Agent = await Language.load(AGENT_WASM);
31
+ const DescriptionLang = await Language.load(DESCRIPTION_WASM);
32
32
  const Behavior = await Language.load(BEHAVIOR_WASM);
33
- agentParser = new Parser();
34
- agentParser.setLanguage(Agent);
33
+ descriptionParser = new Parser();
34
+ descriptionParser.setLanguage(DescriptionLang);
35
35
  behaviorParser = new Parser();
36
36
  behaviorParser.setLanguage(Behavior);
37
37
  }
@@ -42,7 +42,7 @@ const cache = new Map();
42
42
  function parse(uri, langId, text, version) {
43
43
  const prev = cache.get(uri);
44
44
  if (prev?.version === version) return prev.tree;
45
- const parser = langId === 'behavior' ? behaviorParser : agentParser;
45
+ const parser = langId === 'behavior' ? behaviorParser : descriptionParser;
46
46
  if (!parser) return null;
47
47
  const tree = parser.parse(text, prev?.tree); // incremental reuse when possible
48
48
  cache.set(uri, { version, tree });
package/server.js CHANGED
@@ -71,7 +71,7 @@ function getTree(doc) {
71
71
 
72
72
  function validate(doc) {
73
73
  const langId = doc.languageId;
74
- if (langId !== 'agent' && langId !== 'behavior') return;
74
+ if (langId !== 'description' && langId !== 'behavior') return;
75
75
  const tree = getTree(doc);
76
76
  const diagnostics = diagnose(langId, tree, doc.getText());
77
77
  connection.sendDiagnostics({ uri: doc.uri, diagnostics });