solid_agent 0.0.0 → 0.2.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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +321 -0
  5. data/Rakefile +32 -0
  6. data/docs/agent-md-spec.md +803 -0
  7. data/docs/parser-design.md +1369 -0
  8. data/docs/registry-api.md +882 -0
  9. data/examples/README.md +60 -0
  10. data/examples/manifests/changelog_writer.agent.md +81 -0
  11. data/examples/manifests/usage.rb +96 -0
  12. data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
  13. data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
  14. data/examples/memory_handoff/usage.rb +45 -0
  15. data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
  16. data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
  17. data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
  18. data/examples/persistent_conversation/usage.rb +51 -0
  19. data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
  20. data/examples/reasoning/usage.rb +52 -0
  21. data/examples/run_tracking/app/agents/report_agent.rb +30 -0
  22. data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
  23. data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
  24. data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
  25. data/examples/run_tracking/usage.rb +85 -0
  26. data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
  27. data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
  28. data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
  29. data/examples/tool_streaming/usage.rb +47 -0
  30. data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
  31. data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
  32. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
  33. data/lib/generators/solid_agent/context/context_generator.rb +124 -0
  34. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
  35. data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
  36. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
  37. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
  38. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
  39. data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
  40. data/lib/generators/solid_agent/install/install_generator.rb +92 -0
  41. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
  42. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
  43. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  44. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  45. data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
  46. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  47. data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
  48. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
  49. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  50. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
  51. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  52. data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
  53. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  54. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  55. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  56. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  57. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  58. data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
  59. data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
  60. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  61. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  62. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  63. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  64. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  65. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  66. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  67. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  68. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  69. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  70. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  71. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  72. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  73. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  74. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  75. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  76. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  77. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  78. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  79. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  80. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  81. data/lib/solid_agent/agent_manifest.rb +381 -0
  82. data/lib/solid_agent/engine.rb +16 -0
  83. data/lib/solid_agent/has_context.rb +670 -0
  84. data/lib/solid_agent/has_memory.rb +136 -0
  85. data/lib/solid_agent/has_reasons.rb +230 -0
  86. data/lib/solid_agent/has_tools.rb +257 -0
  87. data/lib/solid_agent/model_naming.rb +42 -0
  88. data/lib/solid_agent/model_pricing.rb +93 -0
  89. data/lib/solid_agent/reasonable/reason.rb +205 -0
  90. data/lib/solid_agent/reasonable.rb +181 -0
  91. data/lib/solid_agent/records/agent.rb +520 -0
  92. data/lib/solid_agent/records/agent_run.rb +520 -0
  93. data/lib/solid_agent/records/agent_template.rb +142 -0
  94. data/lib/solid_agent/records/agent_version.rb +141 -0
  95. data/lib/solid_agent/records/ownable.rb +130 -0
  96. data/lib/solid_agent/records.rb +152 -0
  97. data/lib/solid_agent/run_fingerprint.rb +51 -0
  98. data/lib/solid_agent/streams_tool_updates.rb +178 -0
  99. data/lib/solid_agent/tool_cache.rb +91 -0
  100. data/lib/solid_agent/version.rb +5 -0
  101. data/lib/solid_agent.rb +95 -0
  102. data/sig/solid_agent.rbs +4 -0
  103. metadata +174 -14
@@ -0,0 +1,60 @@
1
+ # SolidAgent Examples
2
+
3
+ Runnable, copy-pasteable examples for every SolidAgent concern. Each
4
+ directory mirrors the layout of a Rails app, so the files can be dropped
5
+ into `app/` as-is and the paths tell you where they belong.
6
+
7
+ Every example assumes the persistence tables and models are installed:
8
+
9
+ ```bash
10
+ bundle add solid_agent
11
+ rails generate solid_agent:install
12
+ rails db:migrate
13
+ ```
14
+
15
+ That generates `AgentContext`, `AgentMessage`, `AgentGeneration`,
16
+ `AgentMemory`, `AgentMemoryEntry` and `AgentRun` into `app/models/`, so they
17
+ are yours to edit — SolidAgent's concerns talk to them through a duck-typed
18
+ contract, not through hard-coded class names.
19
+
20
+ The narrative walkthrough of these examples lives at
21
+ [docs.activeagents.ai/solid_agent/examples](https://docs.activeagents.ai/solid_agent/examples).
22
+
23
+ | Example | Concerns | What it shows |
24
+ |---------|----------|---------------|
25
+ | [persistent_conversation](persistent_conversation) | `HasContext` | A support agent whose conversation survives the request, replayed from the database on every turn |
26
+ | [memory_handoff](memory_handoff) | `HasMemory` | Two agents sharing agent-curated notes about the same subject record |
27
+ | [tool_streaming](tool_streaming) | `HasTools`, `StreamsToolUpdates`, `ToolCache` | Declarative tool schemas, live "what is it doing" updates over ActionCable, cached tool results |
28
+ | [reasoning](reasoning) | `HasReasons`, `Reasonable` | Capturing extended-thinking output and persisting it on generation records |
29
+ | [run_tracking](run_tracking) | `AgentRun`, `RunFingerprint`, `ModelPricing` | Durable run records, an append-only progress stream a UI can poll, cohorts and cost |
30
+ | [manifests](manifests) | `AgentManifest` | Defining an agent in a portable `.agent.md` file and loading it as a class |
31
+
32
+ ## Running the examples
33
+
34
+ The `usage.rb` file in each directory is the console script — the part you
35
+ would paste into `rails console` (or call from a controller/job) once the
36
+ agent files are in place. They are written to be read top to bottom rather
37
+ than executed blind: they hit a provider and write rows.
38
+
39
+ Prefer to try one without spending tokens? Point the agent at the mock
40
+ provider first:
41
+
42
+ ```ruby
43
+ class SupportAgent < ApplicationAgent
44
+ generate_with :mock, model: "mock-gpt-4o-mini"
45
+ end
46
+ ```
47
+
48
+ Persistence, memory, runs and the tool cache all behave identically — only
49
+ the model response changes.
50
+
51
+ ## Related documentation
52
+
53
+ - [SolidAgent overview](https://docs.activeagents.ai/solid_agent)
54
+ - [Conversation context](https://docs.activeagents.ai/solid_agent/context)
55
+ - [Long-term memory](https://docs.activeagents.ai/solid_agent/memory)
56
+ - [Tools, streaming and caching](https://docs.activeagents.ai/solid_agent/tools)
57
+ - [Reasoning](https://docs.activeagents.ai/solid_agent/reasoning)
58
+ - [Runs, cohorts and cost](https://docs.activeagents.ai/solid_agent/runs)
59
+ - [Agent manifests](https://docs.activeagents.ai/solid_agent/manifests)
60
+ - [`.agent.md` specification](../docs/agent-md-spec.md)
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: changelog-writer
3
+ version: 1.0.0
4
+ description: Turns a range of merged pull requests into a release changelog
5
+ author: activeagents
6
+ license: MIT
7
+ tags:
8
+ - writing
9
+ - release
10
+
11
+ model: anthropic/claude-sonnet-4-20250514
12
+ config:
13
+ temperature: 0.3
14
+ max_tokens: 2048
15
+
16
+ input:
17
+ schema:
18
+ repository: "string, The repository the release belongs to"
19
+ from?: "string, Git ref the release starts at"
20
+ to?: "string, Git ref the release ends at"
21
+ audience?: "string(users, operators, contributors), Who the changelog is written for"
22
+
23
+ output:
24
+ format: json
25
+ schema:
26
+ type: object
27
+ properties:
28
+ headline:
29
+ type: string
30
+ entries:
31
+ type: array
32
+ items:
33
+ type: object
34
+ properties:
35
+ title:
36
+ type: string
37
+ category:
38
+ type: string
39
+ breaking:
40
+ type: array
41
+ items:
42
+ type: string
43
+
44
+ tools:
45
+ - name: list_merged_pulls
46
+ description: List pull requests merged between two refs
47
+ inputSchema:
48
+ type: object
49
+ properties:
50
+ repository:
51
+ type: string
52
+ from:
53
+ type: string
54
+ to:
55
+ type: string
56
+ required:
57
+ - repository
58
+
59
+ activeagent:
60
+ class_name: ChangelogWriterAgent
61
+ concerns:
62
+ - has_tools: [list_merged_pulls]
63
+ ---
64
+
65
+ # Changelog Writer
66
+
67
+ You write release changelogs from merged pull requests.
68
+
69
+ ## Instructions
70
+
71
+ 1. Call `list_merged_pulls` for the requested range.
72
+ 2. Group the changes into Added, Changed, Fixed and Removed.
73
+ 3. Write one line per change, in the present tense, describing what a
74
+ reader can now do differently — not which files moved.
75
+ 4. List anything that breaks an existing setup under `breaking`, with the
76
+ migration step spelled out.
77
+ 5. Leave a category out entirely rather than padding it.
78
+
79
+ ## Template
80
+
81
+ Write the changelog for {{ repository }} covering {{ from | default: "the last release" }} to {{ to | default: "HEAD" }}, for an audience of {{ audience | default: "users" }}.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Portable agent manifests — rails console walkthrough.
4
+ #
5
+ # A manifest is an agent definition that lives in a file instead of a class:
6
+ # frontmatter for the model, tools, input/output schemas and framework
7
+ # extensions, Markdown for the instructions. It can be reviewed in a pull
8
+ # request, shipped to another framework, or loaded into a running app.
9
+ #
10
+ # Docs: https://docs.activeagents.ai/solid_agent/manifests
11
+ # Format spec: https://github.com/activeagents/solid_agent/blob/main/docs/agent-md-spec.md
12
+
13
+ path = "examples/manifests/changelog_writer.agent.md"
14
+
15
+ # --- Read ---------------------------------------------------------------
16
+
17
+ manifest = SolidAgent::AgentManifest.parse(path)
18
+ manifest.name # => "changelog-writer"
19
+ manifest.model # => "anthropic/claude-sonnet-4-20250514"
20
+ manifest.tools.map(&:name)
21
+ manifest.instructions # the Markdown body
22
+ manifest.fingerprint # stable digest — the version an agent ran under
23
+
24
+ # `load` takes anything: a path, a URL, a JSON/YAML string, or a Hash.
25
+ SolidAgent::AgentManifest.load("https://example.com/agents/support.agent.md")
26
+ SolidAgent::AgentManifest.load({ name: "quick", model: "openai/gpt-4o-mini" })
27
+
28
+ # --- Validate -----------------------------------------------------------
29
+
30
+ SolidAgent::AgentManifest.validate(path) # => [] when valid
31
+ SolidAgent::AgentManifest.valid?(path) # => true
32
+ SolidAgent::AgentManifest.validate(path, strict: true)
33
+ SolidAgent::AgentManifest.validate!(path) # raises ValidationError
34
+
35
+ # Worth wiring into CI, so a broken manifest fails the build rather than a
36
+ # request:
37
+ Dir["config/agents/**/*.agent.md"].flat_map { |f| SolidAgent::AgentManifest.validate(f) }
38
+
39
+ # --- Build --------------------------------------------------------------
40
+
41
+ # Build an agent class from the manifest. The class arrives configured but
42
+ # not finished: it inherits from ApplicationAgent, includes the concerns
43
+ # the manifest asked for, carries the manifest's tool schemas, and keeps
44
+ # the model, provider and instructions as class attributes.
45
+ klass = SolidAgent::AgentManifest.load_agent(path, class_name: "ChangelogWriterAgent")
46
+
47
+ klass._manifest_provider # => "anthropic"
48
+ klass._manifest_model # => "claude-sonnet-4-20250514"
49
+ klass._manifest_instructions # the Markdown body
50
+ klass._manifest # the Manifest itself, fingerprint included
51
+ klass.new.tools.map { |t| t[:name] } # => ["list_merged_pulls"]
52
+
53
+ # `activeagent.class_name` in the frontmatter names the constant, so
54
+ # passing class_name: here is only needed to override it. Name it either
55
+ # way when persisting context — contexts are keyed by class name, and an
56
+ # anonymous class has none.
57
+
58
+ # What the manifest does not carry is behaviour: actions and tool bodies
59
+ # are still Ruby. Reopen the class and supply them.
60
+ #
61
+ # class ChangelogWriterAgent
62
+ # generate_with _manifest_provider.to_sym, model: _manifest_model
63
+ #
64
+ # def write
65
+ # prompt instructions: _manifest_instructions,
66
+ # message: params[:message],
67
+ # tools: tools
68
+ # end
69
+ #
70
+ # # Declared tools raise NotImplementedError until you define them.
71
+ # def list_merged_pulls(repository:, from: nil, to: nil)
72
+ # GitHub.merged_pulls(repository, from: from, to: to)
73
+ # end
74
+ # end
75
+ #
76
+ # ChangelogWriterAgent.with(message: "Release 1.2.0").write.generate_now
77
+
78
+ # --- Convert ------------------------------------------------------------
79
+
80
+ SolidAgent::AgentManifest.parser_formats # what can be read
81
+ SolidAgent::AgentManifest.exporter_formats # what can be written
82
+
83
+ # Import someone else's definition...
84
+ SolidAgent::AgentManifest.parse("agents.yaml") # CrewAI
85
+ SolidAgent::AgentManifest.parse("basic.prompt") # Google Dotprompt
86
+ SolidAgent::AgentManifest.parse("copilot.prompt.md") # GitHub Copilot
87
+
88
+ # ...and export yours for them.
89
+ SolidAgent::AgentManifest.export(manifest, :dotprompt)
90
+ SolidAgent::AgentManifest.convert(path, :crewai, "tmp/agents.yaml")
91
+
92
+ # --- Provenance ---------------------------------------------------------
93
+
94
+ # What an agent ran under, checksummed — pairs with the provenance
95
+ # HasContext records on every generation.
96
+ SolidAgent::AgentManifest.provenance(manifest)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # First half of a hand-off: an agent that researches a project and writes
4
+ # what it learned to long-term memory.
5
+ #
6
+ # Memory is scoped to (subject record, scope name) — not to the agent class
7
+ # — so anything this agent saves is readable by any other agent working on
8
+ # the same project. `save_memory` and `recall_memory` are ordinary
9
+ # function-calling tools, so the model decides when to write and when to
10
+ # read; you only decide what the subject is.
11
+ #
12
+ # Docs: https://docs.activeagents.ai/solid_agent/memory
13
+ class ResearcherAgent < ApplicationAgent
14
+ include SolidAgent::HasMemory
15
+
16
+ generate_with :openai, model: "gpt-4o-mini"
17
+
18
+ # scope: "default", class_name: "AgentMemory" unless you say otherwise.
19
+ # Use a scope to give one subject independent memory streams
20
+ # (has_memory scope: "competitive_research").
21
+ has_memory
22
+
23
+ def research
24
+ prompt(
25
+ message: "Research #{params[:project].name} and save what a writer would need to know.",
26
+ tools: memory_tool_definitions
27
+ )
28
+ end
29
+
30
+ # memory_subject defaults to params[:memorable], falling back to the
31
+ # HasContext contextable. Override it when the subject is somewhere else
32
+ # — here the project the run is about.
33
+ def memory_subject
34
+ params[:project]
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Second half of the hand-off: a different agent class, same subject.
4
+ #
5
+ # Two ways to pick up what ResearcherAgent left behind:
6
+ #
7
+ # 1. Give the model the recall tool and let it decide (below).
8
+ # 2. Prime the instructions with `memory.to_prompt` so the notes are in
9
+ # context from the first token — cheaper, and the model can't forget
10
+ # to look. `draft_with_primed_memory` does that.
11
+ #
12
+ # Docs: https://docs.activeagents.ai/solid_agent/memory
13
+ class WriterAgent < ApplicationAgent
14
+ include SolidAgent::HasMemory
15
+
16
+ generate_with :openai, model: "gpt-4o-mini"
17
+
18
+ has_memory
19
+
20
+ def draft
21
+ prompt(
22
+ message: "Draft the launch post for #{params[:project].name}.",
23
+ tools: memory_tool_definitions
24
+ )
25
+ end
26
+
27
+ def draft_with_primed_memory
28
+ prompt(
29
+ instructions: [
30
+ "You are a product writer.",
31
+ memory&.to_prompt
32
+ ].compact.join("\n\n"),
33
+ message: "Draft the launch post for #{params[:project].name}.",
34
+ tools: memory_tool_definitions
35
+ )
36
+ end
37
+
38
+ def memory_subject
39
+ params[:project]
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Memory hand-off between two agents — rails console walkthrough.
4
+ #
5
+ # Docs: https://docs.activeagents.ai/solid_agent/memory
6
+
7
+ project = Project.find(1)
8
+
9
+ # The researcher calls save_memory as it works. Each note records which
10
+ # agent class wrote it.
11
+ ResearcherAgent.with(project: project).research.generate_now
12
+
13
+ memory = AgentMemory.for(project)
14
+ memory.recall(limit: 5).map { |e| [ e.source_agent, e.category, e.content ] }
15
+ # => [["ResearcherAgent", "fact", "Ships on the 14th; pricing unchanged"], ...]
16
+
17
+ # A different agent class, later — possibly a different request, job, or
18
+ # deploy — picks the same subject up and reads those notes back.
19
+ WriterAgent.with(project: project).draft.generate_now
20
+
21
+ # Or hand the notes over without spending a tool call, by putting them in
22
+ # the instructions:
23
+ memory.to_prompt
24
+ # => "Memory notes for this subject:\n- Ships on the 14th... (ResearcherAgent)"
25
+
26
+ WriterAgent.with(project: project).draft_with_primed_memory.generate_now
27
+
28
+ # Scopes keep unrelated streams apart on the same subject:
29
+ AgentMemory.for(project, scope: "competitive_research").remember(
30
+ "Competitor X ships a similar feature in Q3",
31
+ source_agent: "MarketAgent",
32
+ category: "fact"
33
+ )
34
+
35
+ # Categories filter recall; entries come back newest first.
36
+ memory.recall(category: "handoff", limit: 10)
37
+
38
+ # Curation is ordinary Active Record — nothing here is append-only by
39
+ # force, so prune when a note goes stale.
40
+ memory.entries.where(category: "task").find_each(&:destroy)
41
+
42
+ # The same tool contract is available to non-agent executors (a platform
43
+ # service, an MCP server) without including the concern:
44
+ SolidAgent::HasMemory.tool_definitions.map { |t| t[:name] }
45
+ # => ["save_memory", "recall_memory"]
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A support agent whose conversation outlives the request.
4
+ #
5
+ # `has_context :conversation, contextual: :user` persists the prompt, the
6
+ # assistant's reply, and the tool exchange in between to the tables the
7
+ # install generator created — agent_contexts, agent_messages and
8
+ # agent_generations — keyed by the record passed as params[:user].
9
+ #
10
+ # The macro defines the methods used below (load_conversation,
11
+ # conversation_messages, conversation_result, conversation_summary, ...).
12
+ # Name the context something else and the methods rename with it.
13
+ #
14
+ # Docs: https://docs.activeagents.ai/solid_agent/context
15
+ class SupportAgent < ApplicationAgent
16
+ include SolidAgent::HasContext
17
+
18
+ generate_with :openai, model: "gpt-4o-mini"
19
+
20
+ # Naming a context also names the models it resolves: :conversation alone
21
+ # would look for Conversation / ConversationMessage / ConversationGeneration
22
+ # and raise NameError on the first request. class_name points it back at
23
+ # what `solid_agent:install` wrote (AgentMessage and AgentGeneration are
24
+ # inferred from it). Want separate tables instead? Run
25
+ # `rails generate solid_agent:context conversation` and drop class_name.
26
+ has_context :conversation, class_name: "AgentContext", contextual: :user
27
+
28
+ # Multi-turn: load the stored conversation, append this turn's question,
29
+ # and send the whole history as the prompt.
30
+ #
31
+ # Nothing here writes to the database. With auto_save on (the default),
32
+ # SolidAgent persists the last prompt message as the user turn and the
33
+ # response as the assistant turn, both after the provider call — so the
34
+ # next request replays this exchange. Add messages by hand only with
35
+ # `auto_save: false`, or the turn is stored twice.
36
+ #
37
+ # `contextual: :user` alone would create the context for you, but it runs
38
+ # after the prompt is built — a conversation has to be loaded *before*
39
+ # that to replay its messages, so load it explicitly here.
40
+ def answer
41
+ load_conversation(contextable: params[:user])
42
+
43
+ prompt messages: conversation_messages + [
44
+ { role: "user", content: params[:message] }
45
+ ]
46
+ end
47
+
48
+ # Contexts are keyed by (contextable, agent_name, action_name), so a
49
+ # second action gets its own row rather than appending to the chat
50
+ # history. To work against an existing conversation from a different
51
+ # entry point, load it by id instead of by contextable.
52
+ def summarize
53
+ load_conversation(context_id: params[:conversation_id])
54
+
55
+ prompt messages: conversation_messages + [
56
+ { role: "user", content: "Summarize this conversation in three bullet points." }
57
+ ]
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The web side of a persisted conversation: one POST per turn, and a show
4
+ # action that renders the history straight out of the database — no session
5
+ # state, no cache, nothing to warm up after a deploy.
6
+ class SupportConversationsController < ApplicationController
7
+ def show
8
+ @conversation = AgentContext
9
+ .for_agent("SupportAgent")
10
+ .for_action("answer")
11
+ .find_by!(contextable: current_user)
12
+
13
+ @messages = @conversation.messages.chronological
14
+ end
15
+
16
+ def create
17
+ response = SupportAgent.with(
18
+ user: current_user,
19
+ message: params.require(:message)
20
+ ).answer.generate_now
21
+
22
+ render json: { reply: response.message.content }
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ You are a support agent.
2
+
3
+ Answer in two short paragraphs at most. When you do not know something, say
4
+ so and name the next step the customer should take rather than guessing.
5
+
6
+ <%# Instructions are always rendered from this template. The action
7
+ template (answer.md.erb) is only a fallback for the message — pass
8
+ `messages:` to prompt, as SupportAgent#answer does, and it is skipped. %>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Persistent conversation — rails console walkthrough.
4
+ #
5
+ # Docs: https://docs.activeagents.ai/solid_agent/context
6
+
7
+ user = User.first
8
+
9
+ # Turn one. The context row, the user message and the assistant message are
10
+ # all written during generate_now.
11
+ SupportAgent.with(user: user, message: "My invoice is wrong").answer.generate_now
12
+
13
+ # Turn two. The agent replays turn one from the database before asking.
14
+ SupportAgent.with(user: user, message: "It's the VAT line").answer.generate_now
15
+
16
+ conversation = AgentContext.for_agent("SupportAgent").find_by(contextable: user)
17
+
18
+ conversation.messages.chronological.map { |m| [ m.role, m.content ] }
19
+ # => [["user", "My invoice is wrong"],
20
+ # ["assistant", "..."],
21
+ # ["user", "It's the VAT line"],
22
+ # ["assistant", "..."]]
23
+
24
+ conversation.total_tokens # cumulative across both turns
25
+ conversation.generations.count # => 2, one row per provider call
26
+
27
+ # Every generation carries what produced it: model, finish reason, token
28
+ # split, duration, raw provider payload, and a provenance snapshot of the
29
+ # agent/prompt/context checksums at the time.
30
+ generation = conversation.generations.last
31
+ generation.model # => "gpt-4o-mini"
32
+ generation.total_tokens
33
+ generation.estimated_cost # => USD estimate via SolidAgent::ModelPricing
34
+ generation.provenance["agent_checksum"]
35
+
36
+ # Thread a distributed trace id through prompt_options and the generation
37
+ # joins up with your telemetry:
38
+ #
39
+ # def answer
40
+ # prompt_options[:trace_id] = Current.trace_id
41
+ # ...
42
+ # end
43
+ #
44
+ AgentGeneration.with_trace("some-trace-id")
45
+ AgentContext.with_trace("some-trace-id")
46
+
47
+ # Reading a conversation back for a UI is plain Active Record — the
48
+ # generated models are yours, scopes included.
49
+ conversation.messages.assistant_messages.last&.content
50
+ AgentContext.for_agent("SupportAgent").recent.limit(10)
51
+ user.agent_contexts if user.respond_to?(:agent_contexts) # add the has_many yourself
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Capturing extended thinking.
4
+ #
5
+ # Models that expose reasoning (Claude's extended thinking, OpenAI's
6
+ # reasoning models) return it alongside the answer. HasReasons collects it
7
+ # on the agent instance and, with `persist: true`, writes it onto the
8
+ # generation record so it survives the request — as long as that model
9
+ # includes SolidAgent::Reasonable and has the columns:
10
+ #
11
+ # rails generate solid_agent:reasons AgentGeneration
12
+ # rails db:migrate
13
+ #
14
+ # Reasoning is model output about its own process: treat it as sensitive.
15
+ # `redact_on_persist: true` keeps the token counts and drops the text.
16
+ #
17
+ # Docs: https://docs.activeagents.ai/solid_agent/reasoning
18
+ class AnalysisAgent < ApplicationAgent
19
+ include SolidAgent::HasContext
20
+ include SolidAgent::HasReasons
21
+
22
+ generate_with :anthropic, model: "claude-sonnet-5"
23
+
24
+ # Reasoning is read off the response, so it has to be handed to
25
+ # capture_reasoning once the provider has answered. Declared *before*
26
+ # has_context on purpose: around callbacks nest in declaration order, so
27
+ # this one wraps HasContext's — and by the time it runs, the generation
28
+ # row that `persist: true` updates has been written.
29
+ around_generation :capture_generation_reasoning
30
+
31
+ has_context contextual: :document
32
+
33
+ has_reasons auto_capture: true, # reasoning_prompt_options asks for thinking
34
+ persist: true, # store on the generation record
35
+ budget_tokens: 10_000, # default thinking budget
36
+ redact_on_persist: false # true stores "[Redacted]" + tokens
37
+
38
+ def analyze
39
+ prompt(
40
+ message: "What risks does this contract create for the buyer?",
41
+ **reasoning_prompt_options # extended_thinking + budget from has_reasons
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def capture_generation_reasoning
48
+ response = yield
49
+ capture_reasoning(response)
50
+ response
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reasoning capture — rails console walkthrough.
4
+ #
5
+ # Docs: https://docs.activeagents.ai/solid_agent/reasoning
6
+
7
+ document = Document.find(1)
8
+
9
+ AnalysisAgent.with(document: document).analyze.generate_now
10
+
11
+ # Persisted (persist: true + SolidAgent::Reasonable on the model). This is
12
+ # what survives the request:
13
+ generation = AgentGeneration.recent.first
14
+ generation.reasoning_content
15
+ generation.reasoning_tokens
16
+ generation.has_reasoning?
17
+ generation.reasoning_summary(length: 120)
18
+ generation.thinking? # reasoning_tokens > 0 on the generated model
19
+
20
+ # In-memory, on the agent instance that ran — reachable from inside an
21
+ # action or a callback, not from the console after the fact:
22
+ #
23
+ # def analyze
24
+ # prompt(...)
25
+ # end
26
+ #
27
+ # def after_response
28
+ # reasons # => [SolidAgent::Reasonable::Reason, ...]
29
+ # last_reasoning&.content
30
+ # total_reasoning_tokens
31
+ # has_reasoning?
32
+ # reasoning_chain # every non-redacted reason, joined
33
+ # reasoning_stats
34
+ # # => { count: 2, total_tokens: 450, total_thinking_time_ms: 1200,
35
+ # # redacted_count: 0, models: ["claude-sonnet-5"] }
36
+ # end
37
+
38
+ # Storing reasoning by hand — from a provider response, or as a note:
39
+ generation.store_reasoning!(response)
40
+ generation.store_reason!(
41
+ SolidAgent::Reasonable::Reason.new(content: "Chose the strict parser", tokens: 0)
42
+ )
43
+
44
+ # Reasoning columns on any generation-shaped model:
45
+ #
46
+ # rails generate solid_agent:reasons MyGeneration \
47
+ # --content_column thinking_trace --tokens_column think_tokens
48
+ #
49
+ # class MyGeneration < ApplicationRecord
50
+ # include SolidAgent::Reasonable
51
+ # reasonable_config column: :thinking_trace, tokens_column: :think_tokens
52
+ # end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The agent the run records. Two details matter for run tracking:
4
+ #
5
+ # - the instructions are a constant, so the executor can fingerprint the
6
+ # exact text a run executed under (see DocumentAnalysisRun),
7
+ # - the caller's trace id is threaded into prompt_options, so the
8
+ # generation row, the context row and the run all carry the same
9
+ # trace_id and can be joined with your telemetry.
10
+ #
11
+ # Docs: https://docs.activeagents.ai/solid_agent/runs
12
+ class ReportAgent < ApplicationAgent
13
+ include SolidAgent::HasContext
14
+
15
+ INSTRUCTIONS = <<~TEXT.freeze
16
+ You are a document analyst. Answer only from the document you are given,
17
+ quote the clause you are relying on, and say plainly when the document
18
+ does not cover the question.
19
+ TEXT
20
+
21
+ generate_with :openai, model: "gpt-4o-mini"
22
+
23
+ has_context contextual: :document
24
+
25
+ def analyze
26
+ prompt_options[:trace_id] = params[:trace_id] if params[:trace_id]
27
+
28
+ prompt instructions: INSTRUCTIONS, message: params[:question]
29
+ end
30
+ end