baresexp 1.0.0__tar.gz

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.
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.log
4
+ .DS_Store
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.5
2
+ Name: baresexp
3
+ Version: 1.0.0
4
+ Summary: Minimal token S-expression format AI agent instruction language
5
+ Project-URL: Homepage, https://github.com/zayarmoekaung/baresexp#readme
6
+ Project-URL: Repository, https://github.com/zayarmoekaung/BareSexp.git
7
+ Project-URL: Issues, https://github.com/zayarmoekaung/baresexp/issues
8
+ Author-email: Zayar Moe Kaung <zayarmoekaung@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: agent,ai,llm,prompt-engineering,s-expression
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+
17
+ # baresexp (Python)
18
+
19
+ Minimal-token S-expression language for AI agent instructions.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install baresexp
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from baresexp import builder
31
+
32
+ result = (
33
+ builder
34
+ .task("research-agent", description="Research and summarize a topic")
35
+ .step("collect", description="Gather sources")
36
+ .tool("web-search", description="Search the web")
37
+ .compile()
38
+ )
39
+
40
+ print(result.baresexp)
41
+ ```
42
+
43
+ ## Minimal mode
44
+
45
+ ```python
46
+ from baresexp import builder
47
+
48
+ result = (
49
+ builder
50
+ .minimal()
51
+ .task("research-agent", description="Research and summarize the latest news")
52
+ .step("collect", description="Gather sources")
53
+ .compile(mode="minimal")
54
+ )
55
+
56
+ print(result.baresexp) # compact output
57
+ print(result.token_count) # estimated token count
58
+ print(result.metadata) # original ↔ shortened mapping
59
+ ```
60
+
61
+ ## API
62
+
63
+ | Function | Description |
64
+ | --------------- | ------------------------------------ |
65
+ | `builder` | Fluent builder entry point |
66
+ | `compile_baresexp(task)` | Compile a Task to S-expression |
67
+ | `parse_baresexp(text)` | Parse S-expression to a Task |
68
+ | `validate_baresexp(obj)` | Validate a Task structure |
69
+ | `minimize_description(text)` | Minimize a description string |
@@ -0,0 +1,53 @@
1
+ # baresexp (Python)
2
+
3
+ Minimal-token S-expression language for AI agent instructions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install baresexp
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from baresexp import builder
15
+
16
+ result = (
17
+ builder
18
+ .task("research-agent", description="Research and summarize a topic")
19
+ .step("collect", description="Gather sources")
20
+ .tool("web-search", description="Search the web")
21
+ .compile()
22
+ )
23
+
24
+ print(result.baresexp)
25
+ ```
26
+
27
+ ## Minimal mode
28
+
29
+ ```python
30
+ from baresexp import builder
31
+
32
+ result = (
33
+ builder
34
+ .minimal()
35
+ .task("research-agent", description="Research and summarize the latest news")
36
+ .step("collect", description="Gather sources")
37
+ .compile(mode="minimal")
38
+ )
39
+
40
+ print(result.baresexp) # compact output
41
+ print(result.token_count) # estimated token count
42
+ print(result.metadata) # original ↔ shortened mapping
43
+ ```
44
+
45
+ ## API
46
+
47
+ | Function | Description |
48
+ | --------------- | ------------------------------------ |
49
+ | `builder` | Fluent builder entry point |
50
+ | `compile_baresexp(task)` | Compile a Task to S-expression |
51
+ | `parse_baresexp(text)` | Parse S-expression to a Task |
52
+ | `validate_baresexp(obj)` | Validate a Task structure |
53
+ | `minimize_description(text)` | Minimize a description string |
@@ -0,0 +1,38 @@
1
+ """Example: full vs minimal mode comparison."""
2
+
3
+ from baresexp import builder
4
+
5
+ full = (
6
+ builder
7
+ .task(
8
+ "research-agent",
9
+ description="Research and summarize the latest news about AI agents and tools for this week",
10
+ )
11
+ .step("collect-context", description="Gather recent sources and extract user-relevant facts")
12
+ .compile(mode="full")
13
+ )
14
+
15
+ minimal = (
16
+ builder
17
+ .minimal()
18
+ .task(
19
+ "research-agent",
20
+ description="Research and summarize the latest news about AI agents and tools for this week",
21
+ )
22
+ .step("collect-context", description="Gather recent sources and extract user-relevant facts")
23
+ .compile(mode="minimal")
24
+ )
25
+
26
+ print("FULL")
27
+ print(full.baresexp)
28
+ print("TOKENS", full.token_count)
29
+ print()
30
+ print("MINIMAL")
31
+ print(minimal.baresexp)
32
+ print("TOKENS", minimal.token_count)
33
+ print()
34
+ print("METADATA")
35
+ for task_id, entries in minimal.metadata.items():
36
+ print(f" {task_id}:")
37
+ for entry in entries:
38
+ print(f" {entry.field_path}: {entry.original!r} -> {entry.shortened!r}")
@@ -0,0 +1,14 @@
1
+ """Example: basic research agent with the builder API."""
2
+
3
+ from baresexp import builder, compile_baresexp
4
+
5
+ task = (
6
+ builder
7
+ .task("research-agent", description="Research a topic and return a concise summary")
8
+ .step("collect-context", description="Gather reputable sources and extract key facts")
9
+ .tool("web-search", description="Search the web for supporting references")
10
+ .build()
11
+ )
12
+
13
+ result = compile_baresexp(task)
14
+ print(result.baresexp)
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "baresexp"
7
+ version = "1.0.0"
8
+ description = "Minimal token S-expression format AI agent instruction language"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Zayar Moe Kaung", email = "zayarmoekaung@gmail.com" },
14
+ ]
15
+ keywords = ["ai", "llm", "agent", "prompt-engineering", "s-expression"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/zayarmoekaung/baresexp#readme"
24
+ Repository = "https://github.com/zayarmoekaung/BareSexp.git"
25
+ Issues = "https://github.com/zayarmoekaung/baresexp/issues"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/baresexp"]
@@ -0,0 +1,37 @@
1
+ """BareSexp — minimal-token S-expression language for AI agent instructions."""
2
+
3
+ from .builder import BareSexpBuilder, builder
4
+ from .compiler import compile_baresexp
5
+ from .minimizer import minimize_description, minimize_field_name
6
+ from .parser import parse_baresexp
7
+ from .types import (
8
+ BareSexpResult,
9
+ CompileOptions,
10
+ MetadataEntry,
11
+ Options,
12
+ OutputSpec,
13
+ OutputMode,
14
+ Step,
15
+ Task,
16
+ Tool,
17
+ )
18
+ from .validator import validate_baresexp
19
+
20
+ __all__ = [
21
+ "BareSexpBuilder",
22
+ "BareSexpResult",
23
+ "CompileOptions",
24
+ "MetadataEntry",
25
+ "Options",
26
+ "OutputMode",
27
+ "OutputSpec",
28
+ "Step",
29
+ "Task",
30
+ "Tool",
31
+ "builder",
32
+ "compile_baresexp",
33
+ "minimize_description",
34
+ "minimize_field_name",
35
+ "parse_baresexp",
36
+ "validate_baresexp",
37
+ ]
@@ -0,0 +1,171 @@
1
+ """Builder — fluent API for constructing BareSexp tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .compiler import compile_baresexp
6
+ from .types import (
7
+ BareSexpResult,
8
+ MetadataStore,
9
+ Options,
10
+ OutputMode,
11
+ OutputSpec,
12
+ Step,
13
+ Task,
14
+ Tool,
15
+ )
16
+
17
+
18
+ # Forward reference for return types
19
+ _OutputMode = OutputMode
20
+
21
+
22
+ class _StepBuilder:
23
+ """Fluent builder for a single step."""
24
+
25
+ def __init__(self, task_data: Task, step: Step, mode: OutputMode) -> None:
26
+ self._task_data = task_data
27
+ self._step = step
28
+ self._mode = mode
29
+
30
+ def tool(
31
+ self,
32
+ name: str = "tool",
33
+ *,
34
+ description: str | None = None,
35
+ input: str | None = None,
36
+ output: str | None = None,
37
+ ) -> _StepBuilder:
38
+ t = Tool(name=name, description=description, input=input, output=output)
39
+ self._step.tools.append(t)
40
+ return self
41
+
42
+ def step(
43
+ self,
44
+ name: str = "step",
45
+ *,
46
+ description: str | None = None,
47
+ input: str | None = None,
48
+ output: str | None = None,
49
+ ) -> _StepBuilder:
50
+ s = Step(name=name, description=description, input=input, output=output)
51
+ self._task_data.steps.append(s)
52
+ return _StepBuilder(self._task_data, s, self._mode)
53
+
54
+ def build(self) -> Task:
55
+ return self._task_data
56
+
57
+ def compile(
58
+ self,
59
+ *,
60
+ mode: OutputMode | None = None,
61
+ preserve_metadata: bool = True,
62
+ condense: bool = False,
63
+ ) -> BareSexpResult:
64
+ return compile_baresexp(
65
+ self._task_data,
66
+ mode=mode or self._mode,
67
+ preserve_metadata=preserve_metadata,
68
+ condense=condense,
69
+ )
70
+
71
+
72
+ class _TaskBuilder:
73
+ """Fluent builder for a task."""
74
+
75
+ def __init__(self, task_data: Task, mode: OutputMode) -> None:
76
+ self._task_data = task_data
77
+ self._mode = mode
78
+
79
+ def step(
80
+ self,
81
+ name: str = "step",
82
+ *,
83
+ description: str | None = None,
84
+ input: str | None = None,
85
+ output: str | None = None,
86
+ ) -> _StepBuilder:
87
+ s = Step(name=name, description=description, input=input, output=output)
88
+ self._task_data.steps.append(s)
89
+ return _StepBuilder(self._task_data, s, self._mode)
90
+
91
+ def tool(
92
+ self,
93
+ name: str = "tool",
94
+ *,
95
+ description: str | None = None,
96
+ input: str | None = None,
97
+ output: str | None = None,
98
+ ) -> _TaskBuilder:
99
+ t = Tool(name=name, description=description, input=input, output=output)
100
+ self._task_data.tools.append(t)
101
+ return self
102
+
103
+ def output(
104
+ self,
105
+ *,
106
+ format: str | None = None,
107
+ schema: str | None = None,
108
+ description: str | None = None,
109
+ ) -> _TaskBuilder:
110
+ self._task_data.output = OutputSpec(
111
+ format=format, schema=schema, description=description
112
+ )
113
+ return self
114
+
115
+ def options(
116
+ self,
117
+ *,
118
+ strict: bool | None = None,
119
+ indent: int | None = None,
120
+ include_metadata: bool | None = None,
121
+ ) -> _TaskBuilder:
122
+ self._task_data.options = Options(
123
+ strict=strict, indent=indent, include_metadata=include_metadata
124
+ )
125
+ return self
126
+
127
+ def build(self) -> Task:
128
+ return self._task_data
129
+
130
+ def compile(
131
+ self,
132
+ *,
133
+ mode: OutputMode | None = None,
134
+ preserve_metadata: bool = True,
135
+ condense: bool = False,
136
+ ) -> BareSexpResult:
137
+ return compile_baresexp(
138
+ self._task_data,
139
+ mode=mode or self._mode,
140
+ preserve_metadata=preserve_metadata,
141
+ condense=condense,
142
+ )
143
+
144
+
145
+ class BareSexpBuilder:
146
+ """Top-level builder entry point."""
147
+
148
+ def __init__(self) -> None:
149
+ self._mode: OutputMode = "full"
150
+
151
+ def minimal(self) -> BareSexpBuilder:
152
+ """Switch to minimal output mode for all subsequent compiles."""
153
+ self._mode = "minimal"
154
+ return self
155
+
156
+ def full(self) -> BareSexpBuilder:
157
+ """Switch to full output mode."""
158
+ self._mode = "full"
159
+ return self
160
+
161
+ def task(
162
+ self,
163
+ name: str = "task",
164
+ *,
165
+ description: str | None = None,
166
+ ) -> _TaskBuilder:
167
+ t = Task(name=name, description=description)
168
+ return _TaskBuilder(t, self._mode)
169
+
170
+
171
+ builder = BareSexpBuilder()
@@ -0,0 +1,159 @@
1
+ """Compiler — serializes typed Task objects into BareSexp text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from .minimizer import minimize_description
9
+ from .types import (
10
+ BareSexpResult,
11
+ CompileOptions,
12
+ MetadataEntry,
13
+ MetadataStore,
14
+ OutputMode,
15
+ Step,
16
+ Task,
17
+ Tool,
18
+ )
19
+
20
+
21
+ def _escape_token(value: str) -> str:
22
+ return value.replace("\\", "\\\\").replace('"', '\\"')
23
+
24
+
25
+ def _format_value(value: Any, mode: OutputMode = "full") -> str:
26
+ if isinstance(value, str):
27
+ normalized = value.strip()
28
+ effective = minimize_description(normalized) if mode == "minimal" else normalized
29
+ if not effective or re.search(r'[\s()"\']', effective, re.UNICODE):
30
+ return f'"{_escape_token(effective)}"'
31
+ return effective
32
+ if isinstance(value, (int, float)):
33
+ return str(value)
34
+ if isinstance(value, bool):
35
+ return "true" if value else "false"
36
+ return "null"
37
+
38
+
39
+ def compile_baresexp(
40
+ task: Task,
41
+ *,
42
+ mode: OutputMode = "full",
43
+ preserve_metadata: bool = True,
44
+ condense: bool = False,
45
+ ) -> BareSexpResult:
46
+ """Compile a Task into BareSexp text."""
47
+ metadata: MetadataStore = {}
48
+ indent = "" if condense else " "
49
+ lines: list[str] = [f"(task {_format_value(task.name, mode)}"]
50
+
51
+ if task.description:
52
+ _add_metadata(metadata, task.name, "description", task.description, mode, preserve_metadata)
53
+ lines.append(f"{indent}(description {_format_value(task.description, mode)})")
54
+
55
+ if task.steps:
56
+ lines.append(f"{indent}(steps")
57
+ for step in task.steps:
58
+ lines.append(_compile_step(step, condense, 2, mode, metadata, preserve_metadata, task.name))
59
+ lines.append(f"{indent})")
60
+
61
+ if task.tools:
62
+ lines.append(f"{indent}(tools")
63
+ for tool in task.tools:
64
+ lines.append(_compile_tool(tool, condense, 2, mode, metadata, preserve_metadata, task.name))
65
+ lines.append(f"{indent})")
66
+
67
+ lines.append(")")
68
+
69
+ baresexp = "".join(lines) if condense else "\n".join(lines)
70
+ token_count = _estimate_token_count(baresexp)
71
+ return BareSexpResult(baresexp=baresexp, metadata=metadata, token_count=token_count)
72
+
73
+
74
+ def _compile_step(
75
+ step: Step,
76
+ condense: bool,
77
+ indent_level: int,
78
+ mode: OutputMode,
79
+ metadata: MetadataStore,
80
+ preserve_metadata: bool,
81
+ task_id: str,
82
+ ) -> str:
83
+ indent = "" if condense else " " * indent_level
84
+ child_indent = "" if condense else " " * (indent_level + 1)
85
+ has_children = bool(step.description or step.tools or step.input or step.output)
86
+ lines: list[str] = [f"{indent}(step {_format_value(step.name, mode)}"]
87
+
88
+ if step.description:
89
+ _add_metadata(metadata, task_id, f"steps.{step.name}.description", step.description, mode, preserve_metadata)
90
+ lines.append(f"{child_indent}(description {_format_value(step.description, mode)})")
91
+
92
+ if step.tools:
93
+ lines.append(f"{child_indent}(tools")
94
+ for tool in step.tools:
95
+ lines.append(_compile_tool(tool, condense, indent_level + 2, mode, metadata, preserve_metadata, task_id))
96
+ lines.append(f"{child_indent})")
97
+
98
+ if step.input:
99
+ lines.append(f"{child_indent}(input {_format_value(step.input, mode)})")
100
+ if step.output:
101
+ lines.append(f"{child_indent}(output {_format_value(step.output, mode)})")
102
+
103
+ if not has_children:
104
+ return f"{indent}(step {_format_value(step.name, mode)})"
105
+
106
+ lines.append(f"{indent})")
107
+ return "".join(lines) if condense else "\n".join(lines)
108
+
109
+
110
+ def _compile_tool(
111
+ tool: Tool,
112
+ condense: bool,
113
+ indent_level: int,
114
+ mode: OutputMode,
115
+ metadata: MetadataStore,
116
+ preserve_metadata: bool,
117
+ task_id: str,
118
+ ) -> str:
119
+ indent = "" if condense else " " * indent_level
120
+ child_indent = "" if condense else " " * (indent_level + 1)
121
+ has_children = bool(tool.description or tool.input or tool.output)
122
+ lines: list[str] = [f"{indent}(tool {_format_value(tool.name, mode)}"]
123
+
124
+ if tool.description:
125
+ _add_metadata(metadata, task_id, f"tools.{tool.name}.description", tool.description, mode, preserve_metadata)
126
+ lines.append(f"{child_indent}(description {_format_value(tool.description, mode)})")
127
+
128
+ if tool.input:
129
+ lines.append(f"{child_indent}(input {_format_value(tool.input, mode)})")
130
+ if tool.output:
131
+ lines.append(f"{child_indent}(output {_format_value(tool.output, mode)})")
132
+
133
+ if not has_children:
134
+ return f"{indent}(tool {_format_value(tool.name, mode)})"
135
+
136
+ lines.append(f"{indent})")
137
+ return "".join(lines) if condense else "\n".join(lines)
138
+
139
+
140
+ def _add_metadata(
141
+ metadata: MetadataStore,
142
+ task_id: str,
143
+ field_path: str,
144
+ original: Any,
145
+ mode: OutputMode,
146
+ preserve_metadata: bool,
147
+ ) -> MetadataEntry | None:
148
+ if not preserve_metadata:
149
+ return None
150
+ shortened = minimize_description(str(original)) if mode == "minimal" else str(original)
151
+ entry = MetadataEntry(original=original, field_path=field_path, shortened=shortened)
152
+ if task_id not in metadata:
153
+ metadata[task_id] = []
154
+ metadata[task_id].append(entry)
155
+ return entry
156
+
157
+
158
+ def _estimate_token_count(value: str) -> int:
159
+ return max(1, int(len(value.split()) * 1.2))
@@ -0,0 +1,40 @@
1
+ """Minimizer — reduces descriptions to compact kebab-case tokens."""
2
+
3
+ import re
4
+
5
+ _STOP_WORDS = frozenset({
6
+ "a", "an", "and", "the", "for", "to", "of", "in", "on", "with",
7
+ "from", "into", "about", "this", "that", "these", "those",
8
+ "latest", "recent", "new", "very", "your", "our", "their",
9
+ "its", "is", "are", "be", "been", "being", "will", "can",
10
+ "could", "should", "would", "use", "using", "used", "or",
11
+ })
12
+
13
+
14
+ def minimize_description(text: str) -> str:
15
+ """Collapse a prose description into a short kebab-case token string."""
16
+ normalized = re.sub(r"[^a-zA-Z0-9\s-]", " ", text)
17
+ normalized = re.sub(r"\s+", " ", normalized).strip()
18
+
19
+ if not normalized:
20
+ return "desc"
21
+
22
+ words = [
23
+ w for w in normalized.split(" ")
24
+ if len(w) > 1 and w.lower() not in _STOP_WORDS
25
+ ]
26
+
27
+ if not words:
28
+ return _kebab(normalized)[:24]
29
+
30
+ core = words[:6]
31
+ return _kebab(" ".join(w.lower() for w in core))
32
+
33
+
34
+ def minimize_field_name(name: str) -> str:
35
+ """Normalize a field name to kebab-case."""
36
+ return _kebab(name)
37
+
38
+
39
+ def _kebab(value: str) -> str:
40
+ return re.sub(r"^-+|-+$", "", re.sub(r"[^a-z0-9]+", "-", value.lower()))[:40]
@@ -0,0 +1,241 @@
1
+ """Parser — turns BareSexp text back into typed objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from .types import OutputSpec, Options, Step, Task, Tool
9
+
10
+
11
+ def _tokenize(input_text: str) -> list[str]:
12
+ tokens: list[str] = []
13
+ index = 0
14
+
15
+ while index < len(input_text):
16
+ char = input_text[index]
17
+ if re.match(r"\s", char, re.UNICODE):
18
+ index += 1
19
+ continue
20
+
21
+ if char in ("(", ")"):
22
+ tokens.append(char)
23
+ index += 1
24
+ continue
25
+
26
+ if char == '"':
27
+ escaped = False
28
+ value = '"'
29
+ index += 1
30
+ while index < len(input_text):
31
+ current = input_text[index]
32
+ if escaped:
33
+ value += current
34
+ escaped = False
35
+ elif current == "\\":
36
+ value += current
37
+ escaped = True
38
+ elif current == '"':
39
+ value += current
40
+ index += 1
41
+ break
42
+ else:
43
+ value += current
44
+ index += 1
45
+ tokens.append(value)
46
+ continue
47
+
48
+ value = ""
49
+ while index < len(input_text) and not re.match(r"[\s()]", input_text[index], re.UNICODE):
50
+ value += input_text[index]
51
+ index += 1
52
+ tokens.append(value)
53
+
54
+ return tokens
55
+
56
+
57
+ def _parse_expression(tokens: list[str], index: int) -> tuple[Any, int]:
58
+ if tokens[index] == "(":
59
+ items: list[Any] = []
60
+ cursor = index + 1
61
+ while cursor < len(tokens) and tokens[cursor] != ")":
62
+ if tokens[cursor] == "(":
63
+ parsed, cursor = _parse_expression(tokens, cursor)
64
+ items.append(parsed)
65
+ else:
66
+ items.append(tokens[cursor])
67
+ cursor += 1
68
+ return items, cursor + 1
69
+
70
+ return tokens[index], index + 1
71
+
72
+
73
+ def _parse_scalar(value: Any) -> str | int | float | bool | None:
74
+ if not isinstance(value, str):
75
+ return value
76
+
77
+ trimmed = value.strip()
78
+ if trimmed.startswith('"') and trimmed.endswith('"'):
79
+ return trimmed[1:-1]
80
+
81
+ if trimmed == "true":
82
+ return True
83
+ if trimmed == "false":
84
+ return False
85
+ if re.match(r"^-?\d+(\.\d+)?$", trimmed):
86
+ return float(trimmed) if "." in trimmed else int(trimmed)
87
+ return trimmed
88
+
89
+
90
+ def _parse_task(parts: list[Any]) -> Task:
91
+ task = Task(
92
+ name=parts[0] if isinstance(parts[0], str) else "task",
93
+ )
94
+
95
+ for child in parts[1:]:
96
+ if not isinstance(child, list):
97
+ continue
98
+
99
+ key = child[0] if child else None
100
+ body = child[1:]
101
+ if not isinstance(key, str):
102
+ continue
103
+
104
+ if key == "description":
105
+ task.description = _parse_scalar(body[0]) if body else None
106
+ elif key == "steps":
107
+ task.steps = [_parse_step(item) for item in body if isinstance(item, list)]
108
+ elif key == "tools":
109
+ task.tools = [_parse_tool(item) for item in body if isinstance(item, list)]
110
+ elif key == "output":
111
+ task.output = _parse_output(body)
112
+ elif key == "options":
113
+ task.options = _parse_options(body)
114
+
115
+ return task
116
+
117
+
118
+ def _parse_step(node: Any) -> Step:
119
+ if not isinstance(node, list):
120
+ return Step()
121
+
122
+ key = node[0] if node else None
123
+ body = node[1:]
124
+ if not isinstance(key, str) or key != "step":
125
+ return Step()
126
+
127
+ step = Step(
128
+ name=body[0] if body and isinstance(body[0], str) else "step",
129
+ )
130
+
131
+ for child in body[1:]:
132
+ if not isinstance(child, list):
133
+ continue
134
+
135
+ field_name = child[0] if child else None
136
+ field_body = child[1:]
137
+ if not isinstance(field_name, str):
138
+ continue
139
+
140
+ if field_name == "description":
141
+ step.description = _parse_scalar(field_body[0]) if field_body else None
142
+ elif field_name == "tools":
143
+ step.tools = [_parse_tool(item) for item in field_body if isinstance(item, list)]
144
+ elif field_name == "input":
145
+ step.input = _parse_scalar(field_body[0]) if field_body else None
146
+ elif field_name == "output":
147
+ step.output = _parse_scalar(field_body[0]) if field_body else None
148
+
149
+ return step
150
+
151
+
152
+ def _parse_tool(node: Any) -> Tool:
153
+ if not isinstance(node, list):
154
+ return Tool()
155
+
156
+ key = node[0] if node else None
157
+ body = node[1:]
158
+ if not isinstance(key, str) or key != "tool":
159
+ return Tool()
160
+
161
+ tool = Tool(
162
+ name=body[0] if body and isinstance(body[0], str) else "tool",
163
+ )
164
+
165
+ for child in body[1:]:
166
+ if not isinstance(child, list):
167
+ continue
168
+
169
+ field_name = child[0] if child else None
170
+ field_body = child[1:]
171
+ if not isinstance(field_name, str):
172
+ continue
173
+
174
+ if field_name == "description":
175
+ tool.description = _parse_scalar(field_body[0]) if field_body else None
176
+ elif field_name == "input":
177
+ tool.input = _parse_scalar(field_body[0]) if field_body else None
178
+ elif field_name == "output":
179
+ tool.output = _parse_scalar(field_body[0]) if field_body else None
180
+
181
+ return tool
182
+
183
+
184
+ def _parse_output(parts: list[Any]) -> OutputSpec:
185
+ output = OutputSpec()
186
+ for child in parts:
187
+ if not isinstance(child, list):
188
+ continue
189
+
190
+ field_name = child[0] if child else None
191
+ field_body = child[1:]
192
+ if not isinstance(field_name, str):
193
+ continue
194
+
195
+ if field_name == "format":
196
+ output.format = _parse_scalar(field_body[0]) if field_body else None
197
+ elif field_name == "schema":
198
+ output.schema = _parse_scalar(field_body[0]) if field_body else None
199
+ elif field_name == "description":
200
+ output.description = _parse_scalar(field_body[0]) if field_body else None
201
+
202
+ return output
203
+
204
+
205
+ def _parse_options(parts: list[Any]) -> Options:
206
+ options = Options()
207
+ for child in parts:
208
+ if not isinstance(child, list):
209
+ continue
210
+
211
+ field_name = child[0] if child else None
212
+ field_body = child[1:]
213
+ if not isinstance(field_name, str):
214
+ continue
215
+
216
+ if field_name == "strict":
217
+ val = _parse_scalar(field_body[0]) if field_body else None
218
+ options.strict = bool(val) if val is not None else None
219
+ elif field_name == "indent":
220
+ val = _parse_scalar(field_body[0]) if field_body else None
221
+ options.indent = int(val) if val is not None else None
222
+ elif field_name == "includeMetadata":
223
+ val = _parse_scalar(field_body[0]) if field_body else None
224
+ options.include_metadata = bool(val) if val is not None else None
225
+
226
+ return options
227
+
228
+
229
+ def parse_baresexp(input_text: str) -> Task:
230
+ """Parse BareSexp text into a Task object."""
231
+ tokens = _tokenize(input_text.strip())
232
+ if not tokens:
233
+ return Task()
234
+
235
+ parsed, _ = _parse_expression(tokens, 0)
236
+ if isinstance(parsed, list):
237
+ head, *body = parsed
238
+ if head == "task":
239
+ return _parse_task(body)
240
+
241
+ return Task()
@@ -0,0 +1,94 @@
1
+ """Type definitions for BareSexp structured objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Literal
7
+
8
+ OutputMode = Literal["full", "minimal"]
9
+
10
+
11
+ @dataclass
12
+ class CompileOptions:
13
+ """Options for compiling a task to BareSexp."""
14
+
15
+ mode: OutputMode = "full"
16
+ preserve_metadata: bool = True
17
+ condense: bool = False
18
+ indent: bool = True
19
+
20
+
21
+ @dataclass
22
+ class MetadataEntry:
23
+ """Stores original and shortened values for a field."""
24
+
25
+ original: object
26
+ field_path: str
27
+ shortened: str | None = None
28
+
29
+
30
+ @dataclass
31
+ class Tool:
32
+ """A tool available to a step or task."""
33
+
34
+ name: str = "tool"
35
+ description: str | None = None
36
+ input: str | None = None
37
+ output: str | None = None
38
+ metadata: list[MetadataEntry] = field(default_factory=list)
39
+
40
+
41
+ @dataclass
42
+ class Step:
43
+ """A step within a task."""
44
+
45
+ name: str = "step"
46
+ description: str | None = None
47
+ tools: list[Tool] = field(default_factory=list)
48
+ input: str | None = None
49
+ output: str | None = None
50
+ metadata: list[MetadataEntry] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class OutputSpec:
55
+ """Output format specification."""
56
+
57
+ format: str | None = None
58
+ schema: str | None = None
59
+ description: str | None = None
60
+ metadata: list[MetadataEntry] = field(default_factory=list)
61
+
62
+
63
+ @dataclass
64
+ class Options:
65
+ """Task-level options."""
66
+
67
+ strict: bool | None = None
68
+ indent: int | None = None
69
+ include_metadata: bool | None = None
70
+
71
+
72
+ @dataclass
73
+ class Task:
74
+ """Top-level BareSexp task document."""
75
+
76
+ name: str = "task"
77
+ description: str | None = None
78
+ steps: list[Step] = field(default_factory=list)
79
+ tools: list[Tool] = field(default_factory=list)
80
+ output: OutputSpec | None = None
81
+ options: Options | None = None
82
+ metadata: list[MetadataEntry] = field(default_factory=list)
83
+
84
+
85
+ @dataclass
86
+ class BareSexpResult:
87
+ """Result of compiling a task to BareSexp text."""
88
+
89
+ baresexp: str
90
+ metadata: dict[str, list[MetadataEntry]]
91
+ token_count: int = 0
92
+
93
+
94
+ MetadataStore = dict[str, list[MetadataEntry]]
@@ -0,0 +1,42 @@
1
+ """Validator — checks structural correctness of BareSexp objects."""
2
+
3
+ from .types import Step, Task, Tool
4
+
5
+
6
+ def validate_baresexp(value: object) -> bool:
7
+ """Return True if value is a structurally valid Task."""
8
+ if not isinstance(value, Task):
9
+ return False
10
+
11
+ if not value.name or not value.name.strip():
12
+ return False
13
+
14
+ if value.steps is not None:
15
+ for step in value.steps:
16
+ if not _is_valid_step(step):
17
+ return False
18
+
19
+ if value.tools is not None:
20
+ for tool in value.tools:
21
+ if not _is_valid_tool(tool):
22
+ return False
23
+
24
+ return True
25
+
26
+
27
+ def _is_valid_step(value: object) -> bool:
28
+ if not isinstance(value, Step):
29
+ return False
30
+ if not value.name or not value.name.strip():
31
+ return False
32
+ if value.tools is not None:
33
+ for tool in value.tools:
34
+ if not _is_valid_tool(tool):
35
+ return False
36
+ return True
37
+
38
+
39
+ def _is_valid_tool(value: object) -> bool:
40
+ if not isinstance(value, Tool):
41
+ return False
42
+ return bool(value.name and value.name.strip())
@@ -0,0 +1,59 @@
1
+ """Minimal mode tests."""
2
+
3
+ import pytest
4
+
5
+ from baresexp import builder
6
+
7
+
8
+ def test_minimal_mode_shortens_output_and_stores_metadata():
9
+ result = (
10
+ builder
11
+ .minimal()
12
+ .task(
13
+ "research-agent",
14
+ description="Research and summarize the latest news about AI agents and tools for this week",
15
+ )
16
+ .step("collect-context", description="Gather recent sources and extract user-relevant facts")
17
+ .compile(mode="minimal")
18
+ )
19
+
20
+ assert len(result.baresexp) < 220
21
+ assert result.token_count > 0
22
+ assert "research-agent" in result.metadata
23
+ assert result.metadata["research-agent"][0].original == (
24
+ "Research and summarize the latest news about AI agents and tools for this week"
25
+ )
26
+
27
+
28
+ def test_minimal_vs_full_token_count():
29
+ full = (
30
+ builder
31
+ .task(
32
+ "florida-man-news",
33
+ description="Summarize recent Florida Man news stories into a concise briefing",
34
+ )
35
+ .compile(mode="full")
36
+ )
37
+
38
+ minimal = (
39
+ builder
40
+ .minimal()
41
+ .task(
42
+ "florida-man-news",
43
+ description="Summarize recent Florida Man news stories into a concise briefing",
44
+ )
45
+ .compile(mode="minimal")
46
+ )
47
+
48
+ assert minimal.token_count < full.token_count
49
+
50
+
51
+ def test_condense_removes_whitespace():
52
+ result = (
53
+ builder
54
+ .task("compact-task", description="A task with condense enabled")
55
+ .compile(condense=True)
56
+ )
57
+
58
+ assert "\n" not in result.baresexp
59
+ assert " " not in result.baresexp
@@ -0,0 +1,68 @@
1
+ """Round-trip tests: compile -> parse -> validate."""
2
+
3
+ import pytest
4
+
5
+ from baresexp import builder, compile_baresexp, parse_baresexp, validate_baresexp
6
+
7
+
8
+ def test_builder_compiles_and_round_trips():
9
+ task = (
10
+ builder
11
+ .task("research-agent", description="Research, summarize, and report")
12
+ .step("collect-context", description="Gather relevant sources")
13
+ .tool("web-search", description="Search the web for references")
14
+ .build()
15
+ )
16
+
17
+ bare = compile_baresexp(task)
18
+ parsed = parse_baresexp(bare.baresexp)
19
+
20
+ assert validate_baresexp(parsed) is True
21
+ assert parsed.name == "research-agent"
22
+ assert parsed.steps[0].name == "collect-context"
23
+ assert parsed.steps[0].tools[0].name == "web-search"
24
+
25
+
26
+ def test_parse_preserves_description():
27
+ task = (
28
+ builder
29
+ .task("news-summary", description="Summarize today's top stories")
30
+ .build()
31
+ )
32
+
33
+ bare = compile_baresexp(task)
34
+ parsed = parse_baresexp(bare.baresexp)
35
+
36
+ assert parsed.description == "Summarize today's top stories"
37
+
38
+
39
+ def test_parse_task_with_tools():
40
+ task = (
41
+ builder
42
+ .task("multi-tool-task")
43
+ .tool("search", description="Search the web")
44
+ .tool("read", description="Read a URL")
45
+ .build()
46
+ )
47
+
48
+ bare = compile_baresexp(task)
49
+ parsed = parse_baresexp(bare.baresexp)
50
+
51
+ assert len(parsed.tools) == 2
52
+ assert parsed.tools[0].name == "search"
53
+ assert parsed.tools[1].name == "read"
54
+
55
+
56
+ def test_step_with_input_output():
57
+ task = (
58
+ builder
59
+ .task("pipeline")
60
+ .step("fetch", description="Fetch data", input="url", output="raw_data")
61
+ .build()
62
+ )
63
+
64
+ bare = compile_baresexp(task)
65
+ parsed = parse_baresexp(bare.baresexp)
66
+
67
+ assert parsed.steps[0].input == "url"
68
+ assert parsed.steps[0].output == "raw_data"